Skip to main content

ast-grep-practice

Operate ast-grep as a project lint tool. Covers sgconfig.yml, fix/rewrite rules, constraints, transform, testing, and CI. Use when writing rules ast-grep can express but general-purpose linters cannot.

Source facts

Repository
mizchi/skills
Last source activity
September 16, 2026 at 17:46
Detected SKILL.md language
English
Stars
356
Forks
5

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
7 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
ast-grep-practice
description
Operate ast-grep as a project lint tool. Covers sgconfig.yml, fix/rewrite rules, constraints, transform, testing, and CI. Use when writing rules ast-grep can express but general-purpose linters cannot.
# ast-grep Practice 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. ## Installation ```bash # 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. ## Quick Start Minimum configuration to verify operation: ```bash 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 ``` ## Principles - First check whether an existing linter can cover the rule - Use ast-grep when you need "structural patterns" - Develop rules with TDD: test-first → rule implementation → CI integration - Write `fix` when you can. Prefer rules with auto-fix over detection-only rules ## Choosing between ast-grep and existing linters | 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: - The rule cannot be expressed by configuring existing rules - It depends on parent/child/sibling AST relationships - Automatic rewriting (migration) is required ## Project setup ### sgconfig.yml ```yaml 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'] ``` ### Directory layout ``` 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`. ## Rule file structure ```yaml 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" ``` ### Fields | 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 | ### Suppression comments ```typescript // ast-grep-ignore someCode() // ast-grep-ignore: no-direct-env-access process.env.NODE_ENV ``` ## Metavariable pitfalls 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 contents - A metavariable must occupy a whole node: `obj.on$EVENT` and `"hello $NAME"` do not work ## fix (auto-fix) ### Deciding whether to attach a fix `fix` is convenient, but since it is applied automatically it can change semantics. Do **not** attach one (detection-only) when: - The rewrite changes type safety (e.g. `as any` → `as unknown` changes type-inference results) - Side effects or evaluation order may change (short-circuit behavior, timing of exceptions) - Context dependence means the correct replacement is not unique (e.g. API migrations that swap argument order; review is mandatory) - The deletion entangles with other expressions in the same statement 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". ### Basics ```yaml rule: pattern: console.log($ARG) fix: logger.info($ARG) ``` Metavariables are usable as-is inside the `fix` template. Unmatched metavariables become empty strings. ### Deletion ```yaml 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. ### Fix when bundling multiple patterns with `any:` If each branch under `any:` can use the **same fix template**, it is fine to consolidate into one rule: ```yaml 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). ### Multi-line ```yaml rule: pattern: | def foo($X): $$$S fix: |- def bar($X): $$$S ``` Indentation is preserved relative to the original code's position. ### Range expansion (FixConfig) When you want to include the trailing comma etc. in the deletion: ```yaml fix: template: '' expandEnd: regex: ',' ``` ### Quick rewrites from the CLI ```bash ast-grep run --pattern 'oldFunc($$$ARGS)' --rewrite 'newFunc($$$ARGS)' --lang typescript . # --update-all applies everywhere without confirmation ``` ## constraints 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)**: - You want to constrain **the contents of a metavariable** → `constraints` (e.g. `$METHOD` is one of `get` / `set` / `delete`) - You want to constrain **the structure outside or inside the pattern** → `has` / `inside` / `not` / `precedes` / `follows` (e.g. inside a specific parent, or having a specific child) - If you can write the concrete literal directly in the pattern, that is the simplest (`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. ```yaml 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. ## transform Textually transform matched metavariables before using them in `fix`. ### replace (regex replacement) ```yaml transform: NEW_NAME: replace: source: $NAME replace: 'get(\w+)' by: 'fetch$1' fix: $NEW_NAME($$$ARGS) ``` ### substring ```yaml transform: INNER: substring: source: $STR startChar: 1 endChar: -1 ``` Negative indices count from the end. Same semantics as Python slicing. ### convert (case conversion) ```yaml transform: SNAKE: convert: source: $NAME toCase: snakeCase separatedBy: [caseChange] ``` Supported cases: `camelCase`, `snakeCase`, `kebabCase`, `pascalCase`, `upperCase`, `lowerCase`, `capitalize` ### rewrite (experimental) Recursively rewrite nodes inside a metavariable using rewriter rules. ```yaml transform: REWRITTEN: rewrite: source: $$$BODY rewriters: [migrate-api-call] joinBy: "\n" ``` ## utils (utility rules) Reference shared rules defined under `utilDirs` with `matches`. ```yaml # 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 ``` ```yaml # 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 ``` ## Testing There are two kinds of tests. Do not conflate them: - **Classification test** (`test --skip-snapshot-tests`): only verifies that the code listed under `valid` / `invalid` is classified correctly. This is the one to run in CI. - **Snapshot test** (`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. ### Test file format The `id` inside the test file must match the `id` of the rule file. The filename is free (convention: `{rule-id}-test.yml`). ```yaml # 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 ``` ### Running tests ```bash # 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: - `.` : pass - `N` : noisy (false positive — matches valid code) - `M` : missing (false negative — fails to match invalid code) ### Workflow 1. Write the test file under `rule-tests/` (Red) 2. Write the rule under `rules/` (Green) 3. Verify with `ast-grep test --skip-snapshot-tests` 4. Generate snapshots with `ast-grep test -U` 5. Review the snapshots and commit ## CI integration ### justfile ```just ast-grep-test: ast-grep test ast-grep-lint: ast-grep scan check: format-check typecheck ast-grep-lint test ``` ### GitHub Actions Align tools with the dev environment (use pnpm in CI if the project uses pnpm, npm if it uses npm): ```yaml - 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 ```
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub