| name | ast-grep-setup |
| description | Set up ast-grep for a codebase with common TypeScript rules for detecting anti-patterns, enforcing best practices, and preventing bugs. Creates sgconfig.yml, rule files, and rule tests. Use when adding structural linting, banning legacy patterns, or implementing ratchet gates. |
ast-grep Setup Skill
Set up ast-grep with common TypeScript rules for detecting anti-patterns, enforcing best practices, and preventing bugs.
When to Use
- Adding structural linting to a TypeScript codebase
- Banning legacy patterns after migration
- Implementing ratchet gates (block new violations while grandfathering existing ones)
- Enforcing architecture boundaries
- Preventing common TypeScript/JavaScript bugs
CRITICAL: Read YAML Structure First
Agents: Before writing any rules, read the YAML Structure Rules section.
The most common mistakes are:
- Putting
constraints inside rule: (must be at top level)
- Duplicate YAML keys like multiple
not: blocks
- Not wrapping multiple conditions in
all: or any:
Use the validation script to catch these before running ast-grep:
python3 scripts/validate-rule.py rules/*.yml
Quick Start
1. Initialize ast-grep Configuration
mkdir -p rules/ast-grep/rules rules/ast-grep/rule-tests
cat > rules/ast-grep/sgconfig.yml << 'EOF'
ruleDirs:
- rules
testConfigs:
- testDir: rule-tests
allowedFixers: []
EOF
2. Add Rules for Common TypeScript Pain Points
See the Rule Library below for ready-to-use rules.
3. Run and Test
cd rules/ast-grep && ast-grep scan
cd rules/ast-grep && ast-grep test
cd rules/ast-grep && ast-grep test -U
Rule Library
Type Safety Rules
no-implicit-any-params
Prevents function parameters without explicit types (implicit any).
id: no-implicit-any-params
language: TypeScript
severity: warning
message: "Parameter '$PARAM' lacks explicit type annotation"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
pattern: function $FUNC($PARAM) { $$$BODY }
constraints:
PARAM:
not:
has:
kind: type_annotation
no-unsafe-any-usage
Bans direct property access on any typed values.
id: no-unsafe-any-usage
language: TypeScript
severity: error
message: "Unsafe property access on 'any' type. Cast or add type guard."
rule:
pattern: $EXPR.$PROP
constraints:
EXPR:
typeAnnotation: any
Async/Await Rules
no-floating-promises
Prevents unhandled promises that could fail silently.
id: no-floating-promises
language: TypeScript
severity: error
message: "Promise is not awaited, returned, or handled with .catch()"
files:
- src/**/*.ts
- src/**/*.tsx
ignores:
- new Promise($$$)
- Promise.$FUNC($$$)
rule:
all:
- pattern: $PROMISE_FUNC($$$ARGS)
- has:
kind: call_expression
pattern: $PROMISE_FUNC($$$ARGS)
- not:
inside:
kind: await_expression
stopBy: end
- not:
inside:
kind: return_statement
stopBy: end
- not:
inside:
kind: call_expression
pattern: $$$.catch($$$)
stopBy: end
constraints:
PROMISE_FUNC:
regex: (fetch|axios\.[a-z]+|async|\.then)
no-missing-await
Detects async function calls without await.
id: no-missing-await
language: TypeScript
severity: warning
message: "Async function '$FUNC' called without await"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
all:
- pattern: $FUNC($$$ARGS)
- not:
inside:
kind: await_expression
stopBy: end
- not:
inside:
kind: return_statement
stopBy: end
constraints:
FUNC:
typeAnnotation: /^Promise</
Error Handling Rules
no-empty-catch
Bans empty catch blocks that swallow errors.
id: no-empty-catch
language: TypeScript
severity: error
message: "Empty catch block silently swallows errors. Log or re-throw."
files:
- src/**/*.ts
- src/**/*.tsx
rule:
all:
- pattern: try { $$$TRY } catch ($ERROR) { $$$CATCH }
- not:
has:
kind: statement
inside:
kind: catch_clause
pattern: $$$CATCH
#### require-error-logging
Requires error logging in catch blocks.
```yaml
# rules/require-error-logging.yml
id: require-error-logging
language: TypeScript
severity: warning
message: "Catch block should log the error"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
all:
- pattern: try { $$$TRY } catch ($ERR) { $$$CATCH }
- not:
has:
kind: call_expression
pattern: console.$LOG($ERR)
inside:
kind: catch_clause
pattern: $$$CATCH
- not:
has:
kind: call_expression
pattern: logger.$LOG($ERR)
inside:
kind: catch_clause
pattern: $$$CATCH
React Rules
no-use-effect-missing-deps
Flags useEffect hooks that might be missing dependencies.
id: no-use-effect-missing-deps
language: TypeScript
severity: warning
message: "useEffect has an empty dependency array but references external values"
files:
- src/**/*.tsx
rule:
pattern: useEffect($FUNC, [])
constraints:
FUNC:
has:
kind: identifier
pattern: $ID
not:
pattern: console
no-direct-state-mutation
Prevents direct state mutation in React.
id: no-direct-state-mutation
language: TypeScript
severity: error
message: "Do not mutate state directly. Use the setter function."
files:
- src/**/*.tsx
rule:
all:
- pattern: $STATE.$PROP = $VAL
- has:
kind: identifier
pattern: $STATE
regex: ^set[A-Z]
Performance Rules
no-array-reduce-for-objects
Warns about using reduce to build objects (often less readable).
id: no-array-reduce-for-objects
language: TypeScript
severity: warning
message: "Consider using Object.fromEntries() or a for...of loop instead of reduce for building objects"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
pattern: $ARR.reduce(($ACC, $ITEM) => { $$$BODY; return $ACC; }, {})
no-regex-in-loop
Prevents regex creation inside loops (compiles on each iteration).
id: no-regex-in-loop
language: TypeScript
severity: warning
message: "Creating regex inside loop - move outside or use constant"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
inside:
kind: for_statement
stopBy: end
pattern: /$PAT/
Architecture Rules
no-cross-module-imports
Enforces module boundaries (customize for your architecture).
id: no-cross-module-imports
language: TypeScript
severity: error
message: "Domain modules should not import from other domain modules directly"
files:
- src/domain/**/*.ts
rule:
all:
- pattern: import $$$ from "$MOD"
- matches:
source: $MOD
contains: /domain/
no-node-in-frontend
Prevents Node.js modules from being imported in frontend code.
id: no-node-in-frontend
language: TypeScript
severity: error
message: "Node.js built-in modules cannot be used in frontend code"
files:
- src/frontend/**/*.ts
- src/frontend/**/*.tsx
- src/client/**/*.ts
- src/client/**/*.tsx
rule:
all:
- pattern: import $$$ from "$MOD"
- matches:
source: $MOD
regex: ^(fs|path|os|crypto|http|https|net|dgram|dns|cluster|module|vm|child_process|worker_threads)$
Best Practice Rules
no-console-log
Prevents console.log in production code (use a logger instead).
id: no-console-log
language: TypeScript
severity: warning
message: "Use a proper logger instead of console.log"
files:
- src/**/*.ts
- src/**/*.tsx
ignores:
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/__tests__/**"
rule:
pattern: console.log($$$ARGS)
no-debugger
Prevents debugger statements.
id: no-debugger
language: TypeScript
severity: error
message: "Remove debugger statement before committing"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
pattern: debugger;
prefer-const-over-let
Suggests const when variable is never reassigned.
id: prefer-const-over-let
language: TypeScript
severity: hint
message: "Consider using 'const' since this variable is never reassigned"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
all:
- pattern: let $VAR = $INIT
- not:
follows:
pattern: $VAR = $NEWVAL
stopBy: end
Rule Tests
Each rule should have a corresponding test file:
Example: no-floating-promises-test.yml
id: no-floating-promises
valid:
- |
const result = await fetch('/api/users');
- |
return fetch('/api/users');
- |
fetch('/api/users').catch(err => console.error(err));
- |
new Promise((resolve) => setTimeout(resolve, 1000));
- |
Promise.all([fetch('/a'), fetch('/b')]);
invalid:
- |
function getUsers() {
fetch('/api/users');
}
- |
async function load() {
fetch('/api/data');
}
Advanced Patterns
Ratchet Mode: Allow Existing Violations
To implement a ratchet (block new violations while allowing existing ones):
ast-grep scan --json > baseline/violations.json
cat > tools/ast-grep/baseline-check.sh << 'SCRIPT'
ast-grep scan --json | node -e '
const baseline = require("./baseline/violations.json");
const current = JSON.parse(require("fs").readFileSync(0, "utf-8"));
const baselineSet = new Set(baseline.map(v => `${v.file}:${v.line}:${v.ruleId}`));
const newViolations = current.filter(v => !baselineSet.has(`${v.file}:${v.line}:${v.ruleId}`));
if (newViolations.length > 0) {
console.error("New violations found:");
newViolations.forEach(v => console.error(`${v.file}:${v.line} - ${v.message}`));
process.exit(1);
}
'
SCRIPT
chmod +x tools/ast-grep/baseline-check.sh
CI Integration
name: ast-grep
on: [push, pull_request]
jobs:
ast-grep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
- name: Install ast-grep
run: npm install -g @ast-grep/cli
- name: Run ast-grep scan
run: cd rules/ast-grep && ast-grep scan
- name: Run rule tests
run: cd rules/ast-grep && ast-grep test
IDE Integration
VS Code: Install the "ast-grep" extension for inline highlighting.
Rule Writing Tips
- Start simple: Use basic patterns first, then add constraints
- Test edge cases: Create comprehensive rule-tests
- Use constraints: Filter by typeAnnotation, regex, kind
- Check inside/outside: Use
inside, follows, precedes for context
- StopBy carefully: Control matching scope with
stopBy: end or stopBy: neighbor
Common Pattern Reference
| Pattern | Matches |
|---|
function $FUNC($$$PARAMS) { $$$BODY } | Function declarations |
const $VAR = $EXPR | Const declarations |
$EXPR.$PROP | Property access |
import $$$ from "$MOD" | Import statements |
export $KIND $NAME | Export declarations |
$FUNC($$$ARGS) | Function calls |
await $EXPR | Await expressions |
try { $$$TRY } catch ($ERR) { $$$CATCH } | Try-catch |
$ARR.map($FN) | Array methods |
Critical: YAML Structure Rules
STOP: Read this before writing rules. These are the most common mistakes agents make.
1. constraints Goes at TOP LEVEL
WRONG - constraints inside rule:
id: bad-example
rule:
pattern: import $NAME from $MOD
constraints:
MOD:
regex: "fs"
RIGHT - constraints at top level:
id: good-example
rule:
pattern: import $NAME from $MOD
constraints:
MOD:
regex: "fs"
2. No Duplicate Keys in YAML
WRONG - duplicate not: keys:
rule:
all:
- pattern: $FUNC($$$ARGS)
- not:
inside:
kind: await_expression
- not:
inside:
kind: return_statement
RIGHT - wrap in all: with separate patterns:
rule:
all:
- pattern: $FUNC($$$ARGS)
- not:
inside:
kind: await_expression
stopBy: end
- not:
inside:
kind: return_statement
stopBy: end
Or use any: for alternatives:
rule:
any:
- pattern: fetch($$$)
- pattern: axios.$METHOD($$$)
3. Proper all: / any: Structure
Pattern: When you need multiple conditions or multiple negations, always wrap in all: or any:.
rule:
all:
- pattern: $FUNC($$$ARGS)
- has:
kind: call_expression
- not:
inside:
kind: await_expression
rule:
any:
- pattern: console.log($$$)
- pattern: console.warn($$$)
- pattern: console.error($$$)
4. Top-Level Rule File Structure
id: rule-id
language: TypeScript
severity: error
message: "Error message"
files:
- src/**/*.ts
ignores:
- "**/*.test.ts"
rule:
pattern: ...
all: []
any: []
constraints:
VAR_NAME:
regex: "pattern"
utils:
MY_UTIL:
pattern: ...
5. Validate Before Running
Always validate your YAML structure before testing:
python3 -c "import yaml; yaml.safe_load(open('rules/my-rule.yml'))" && echo "YAML OK"
python3 scripts/validate-rule.py rules/my-rule.yml
cd rules/ast-grep && ast-grep scan
6. Common Error Messages
| Error | Cause | Fix |
|---|
missing field constraints | constraints inside rule | Move to top level |
yaml.scanner.ScannerError | Duplicate keys | Use all: wrapper |
unknown variant | Invalid enum value | Check docs for valid values |
did not find expected key | Indentation error | Check YAML indentation |
Rule Validation Script
Use this script to validate rule files before committing:
"""Validate ast-grep rule YAML structure."""
import yaml
import sys
from pathlib import Path
def validate_rule(file_path):
"""Validate a single rule file."""
content = Path(file_path).read_text()
data = yaml.safe_load(content)
errors = []
required = ['id', 'language', 'severity', 'message', 'rule']
for field in required:
if field not in data:
errors.append(f"Missing required field: {field}")
if 'rule' in data and isinstance(data['rule'], dict):
if 'constraints' in data['rule']:
errors.append("constraints inside rule: - must be at TOP LEVEL")
if 'utils' in data['rule']:
errors.append("utils inside rule: - must be at TOP LEVEL")
if 'transform' in data['rule']:
errors.append("transform inside rule: - must be at TOP LEVEL")
lines = content.split('\n')
for i, line in enumerate(lines, 1):
stripped = line.lstrip()
if stripped.startswith('- '):
continue
if ':' in stripped:
key = stripped.split(':')[0]
if errors:
print(f"❌ {file_path}")
for err in errors:
print(f" - {err}")
return False
else:
print(f"✓ {file_path}")
return True
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: validate-rule.py <rule-file.yml> [rule-file2.yml ...]")
sys.exit(1)
all_valid = True
for path in sys.argv[1:]:
if not validate_rule(path):
all_valid = False
sys.exit(0 if all_valid else 1)
Save as scripts/validate-rule.py and run:
python3 scripts/validate-rule.py rules/*.yml
Pre-commit Hook
Add this to .git/hooks/pre-commit or .pre-commit-config.yaml:
#!/bin/bash
RULES_DIR="rules/ast-grep/rules"
if [ -d "$RULES_DIR" ]; then
echo "Validating ast-grep rules..."
if ! python3 scripts/validate-rule.py "$RULES_DIR"/*.yml; then
echo ""
echo "❌ Rule validation failed. Fix YAML structure errors before committing."
echo " See skills/ast-grep-setup/SKILL.md for YAML structure rules."
exit 1
fi
fi
exit 0
Make it executable:
chmod +x .git/hooks/pre-commit
GitHub Actions Validation
Add this job to validate rules in CI:
name: Validate ast-grep Rules
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install pyyaml
run: pip install pyyaml
- name: Validate rule files
run: python3 scripts/validate-rule.py rules/ast-grep/rules/*.yml