--- 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](#critical-yaml-structure-rules) section.** The most common mistakes are: 1. Putting `constraints` inside `rule:` (must be at top level) 2. Duplicate YAML keys like multiple `not:` blocks 3. Not wrapping multiple conditions in `all:` or `any:` Use the validation script to catch these before running ast-grep: ```bash python3 scripts/validate-rule.py rules/*.yml ``` ## Quick Start ### 1. Initialize ast-grep Configuration ```bash # Create sgconfig.yml in project root 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](#rule-library) below for ready-to-use rules. ### 3. Run and Test ```bash # Scan the codebase cd rules/ast-grep && ast-grep scan # Run rule tests cd rules/ast-grep && ast-grep test # Update test snapshots after rule changes 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). ```yaml # rules/no-implicit-any-params.yml 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. ```yaml # rules/no-unsafe-any-usage.yml 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. ```yaml # rules/no-floating-promises.yml 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. ```yaml # rules/no-missing-await.yml 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 { $$$BODY; return $ACC; }, {}) ``` #### no-regex-in-loop Prevents regex creation inside loops (compiles on each iteration). ```yaml # rules/no-regex-in-loop.yml 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). ```yaml # rules/no-cross-module-imports.yml 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. ```yaml # rules/no-node-in-frontend.yml 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). ```yaml # rules/no-console-log.yml 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. ```yaml # rules/no-debugger.yml 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. ```yaml # rules/prefer-const-over-let.yml 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 ```yaml 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): ```bash # 1. Generate baseline of current violations ast-grep scan --json > baseline/violations.json # 2. Create baseline extractor script cat > tools/ast-grep/baseline-check.sh << 'SCRIPT' #!/bin/bash # Check only new violations against baseline 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 ```yaml # .github/workflows/ast-grep.yml 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 1. **Start simple**: Use basic patterns first, then add constraints 2. **Test edge cases**: Create comprehensive rule-tests 3. **Use constraints**: Filter by typeAnnotation, regex, kind 4. **Check inside/outside**: Use `inside`, `follows`, `precedes` for context 5. **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:** ```yaml id: bad-example rule: pattern: import $NAME from $MOD constraints: # ❌ WRONG: constraints inside rule MOD: regex: "fs" ``` **RIGHT - constraints at top level:** ```yaml id: good-example rule: pattern: import $NAME from $MOD constraints: # ✓ CORRECT: constraints at root level MOD: regex: "fs" ``` ### 2. No Duplicate Keys in YAML **WRONG - duplicate `not:` keys:** ```yaml rule: all: - pattern: $FUNC($$$ARGS) - not: # ❌ First not inside: kind: await_expression - not: # ❌ DUPLICATE KEY - YAML will only keep one! inside: kind: return_statement ``` **RIGHT - wrap in `all:` with separate patterns:** ```yaml rule: all: - pattern: $FUNC($$$ARGS) - not: inside: kind: await_expression stopBy: end - not: inside: kind: return_statement stopBy: end ``` Or use `any:` for alternatives: ```yaml 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:`. ```yaml # Multiple conditions all must match rule: all: - pattern: $FUNC($$$ARGS) - has: kind: call_expression - not: inside: kind: await_expression # Any of these patterns match rule: any: - pattern: console.log($$$) - pattern: console.warn($$$) - pattern: console.error($$$) ``` ### 4. Top-Level Rule File Structure ```yaml id: rule-id # Required: unique identifier language: TypeScript # Required: target language severity: error # Required: error, warning, hint, info message: "Error message" # Required: user-facing message files: # Optional: glob patterns - src/**/*.ts ignores: # Optional: exclusion patterns - "**/*.test.ts" rule: # Required: the matching rule pattern: ... # Basic pattern # OR all: [] # Multiple conditions # OR any: [] # Alternative patterns constraints: # Optional: variable constraints (TOP LEVEL!) VAR_NAME: regex: "pattern" utils: # Optional: utility patterns MY_UTIL: pattern: ... ``` ### 5. Validate Before Running Always validate your YAML structure before testing: ```bash # Check YAML is valid python3 -c "import yaml; yaml.safe_load(open('rules/my-rule.yml'))" && echo "YAML OK" # Check rule structure python3 scripts/validate-rule.py rules/my-rule.yml # Then run ast-grep 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: ```python #!/usr/bin/env python3 """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 = [] # Check required fields required = ['id', 'language', 'severity', 'message', 'rule'] for field in required: if field not in data: errors.append(f"Missing required field: {field}") # Check constraints at wrong level (common mistake) 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") # Check for duplicate keys by analyzing raw YAML lines = content.split('\n') for i, line in enumerate(lines, 1): stripped = line.lstrip() if stripped.startswith('- '): continue # Skip list items if ':' in stripped: key = stripped.split(':')[0] # This is a simple check - proper parsing would be more robust 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-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: ```bash python3 scripts/validate-rule.py rules/*.yml ``` ### Pre-commit Hook Add this to `.git/hooks/pre-commit` or `.pre-commit-config.yaml`: ```bash #!/bin/bash # .git/hooks/pre-commit - validate ast-grep rules 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: ```bash chmod +x .git/hooks/pre-commit ``` ### GitHub Actions Validation Add this job to validate rules in CI: ```yaml # .github/workflows/validate-ast-grep-rules.yml 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 ```