main

ast-grep Guide for Home Repository

Overview

ast-grep is a fast, structural code search and rewrite tool that operates on Abstract Syntax Trees (AST). Unlike text-based tools like grep or ripgrep, ast-grep understands the syntax of programming languages, making it ideal for pattern-based code analysis and refactoring.

This guide documents the integration and usage of ast-grep in the home repository.

Why ast-grep?

Problems It Solves

  1. Polyglot Codebase: Our repository contains Nix, Bash, Go, Python, YAML, and more. ast-grep works across all these languages with a consistent interface.

  2. Structural vs. Text Search: Finding password = "..." in Nix configuration is different from finding PASSWORD="..." in Bash. ast-grep understands these structural differences.

  3. Safe Refactoring: When refactoring code patterns, text-based replacement can match unintended locations. ast-grep only matches syntactically valid structures.

  4. Custom Linting: We have repository-specific patterns and conventions that generic linters don’t check for.

Advantages

  • Fast: Scans 400+ files in ~0.02 seconds
  • Accurate: AST-based matching eliminates false positives
  • Interactive: Review and apply changes one by one
  • Flexible: From simple CLI searches to complex YAML rules
  • Polyglot: One tool for multiple languages

Installation

ast-grep is already available in the repository via Nix:

# Already in your shell environment
ast-grep --version  # 0.40.5

# Or use via nix-shell
nix-shell -p ast-grep

Quick Start

Find all mkHost calls in flake.nix:

ast-grep -p 'mkHost' -l nix flake.nix

Output:

flake.nix:72:        kyushu = libx.mkHost {
flake.nix:78:        aomi = libx.mkHost {
flake.nix:81:        sakhalin = libx.mkHost {
...

Search with Metavariables

Find all bash functions:

ast-grep -p '$NAME() { $$$ }' -l bash tools/

Metavariables:

  • $VAR - matches single AST node
  • $$$ARGS - matches zero or more nodes

Interactive Rewrite

Replace a pattern across files:

ast-grep -p 'oldPattern' --rewrite 'newPattern' --interactive

Configuration

The repository includes pre-configured rules:

~/src/home/
├── sgconfig.yml              # Main configuration (repo root)
└── .ast-grep/
    └── rules/                # Custom rules
        ├── nix-prefer-inherit.yml
        ├── nix-explicit-option-types.yml
        ├── bash-require-strict-mode.yml
        ├── bash-unsafe-rm-rf.yml
        ├── security-unsafe-curl-pipe-sh.yml
        └── ...

Note: sgconfig.yml must be in the repository root for ast-grep scan to auto-detect it.

Running Linter

Important: Run from repository root (~/src/home)

Scan the entire repository:

cd ~/src/home
ast-grep scan

Scan specific directory:

cd ~/src/home
ast-grep scan systems/

Get JSON output:

cd ~/src/home
ast-grep scan --json

Or specify config path explicitly:

ast-grep scan --config /home/vincent/src/home/.ast-grep/sgconfig.yml

Current Rules

Nix Rules

nix-prefer-inherit (info)

Problem: Redundant attribute assignment when name matches variable.

Bad:

{ pkgs = pkgs; lib = lib; }

Good:

{ inherit pkgs lib; }

nix-explicit-option-types (warning)

Problem: mkOption without explicit type annotation.

Bad:

myOption = mkOption {
  default = "value";
  description = "My option";
};

Good:

myOption = mkOption {
  type = types.str;
  default = "value";
  description = "My option";
};

Current findings: 137 instances across custom modules (see Scan Results)

nix-prefer-optional (info)

Problem: Verbose conditional list construction.

Bad:

if condition then [ item ] else []

Good:

lib.optional condition item

nix-boolean-comparison (warning)

Problem: Comparing booleans to true/false directly.

Bad:

if myBool == true then ...

Good:

if myBool then ...

Bash Rules

bash-require-strict-mode (error)

Problem: Bash scripts without error handling.

Every #!/usr/bin/env bash script should start with:

#!/usr/bin/env bash
set -euo pipefail

Where:

  • set -e: Exit on any command failure
  • set -u: Exit on undefined variable usage
  • set -o pipefail: Exit if any command in a pipeline fails

Current findings: 10 scripts missing strict mode

Files affected:

  • install.sh
  • keyboards/eyelash_corne/go.sh
  • imperative/nagoya/apply.sh
  • And 7 more…

bash-unsafe-rm-rf (error)

Problem: Dangerous rm -rf usage without variable checks.

Bad:

rm -rf $SOME_DIR

Good:

[[ -n "$SOME_DIR" ]] && rm -rf "$SOME_DIR"

Or better:

if [[ -n "$SOME_DIR" && -d "$SOME_DIR" ]]; then
  rm -rf "$SOME_DIR"
fi

Current findings: 5 instances needing review

bash-use-command-over-which (warning)

Problem: Using non-POSIX which command.

Bad:

which git

Good:

command -v git

bash-prefer-dollar-parens (info)

Problem: Using backticks for command substitution.

Bad:

OUTPUT=`date`

Good:

OUTPUT=$(date)

Security Rules

security-unsafe-curl-pipe-sh (error)

Problem: Executing downloaded scripts without review.

Dangerous patterns:

curl https://example.com/install.sh | sh
wget -O- https://example.com/install.sh | bash

This is unsafe because:

  1. No integrity verification (no checksum)
  2. No review of what’s being executed
  3. Vulnerable to MITM attacks
  4. Can’t debug if something goes wrong

Better approach:

# Download
curl -o install.sh https://example.com/install.sh

# Verify checksum
echo "expected_sha256 install.sh" | sha256sum -c

# Review
less install.sh

# Execute
bash install.sh

Current findings: 1 instance in test/example code

Scan Results

Full repository scan (as of 2026-02-09):

Files scanned: 417 (*.nix, *.sh, *.go, *.py, *.yaml, *.json)
Scan time: 0.022 seconds
Issues found: 154

Breakdown by severity:
- Errors: 16
- Warnings: 137
- Info: 1

Breakdown by rule:
- nix-explicit-option-types (warning): 137
- bash-require-strict-mode (error): 10
- bash-unsafe-rm-rf (error): 5
- security-unsafe-curl-pipe-sh (error): 1
- nix-prefer-optional (info): 1

Performance

Operation Time Files/sec
Full scan (417 files) 0.022s ~19,000
Nix files only (~350) 0.018s ~19,400
Bash files only (~50) 0.008s ~6,250

Comparison with other tools:

  • ripgrep (text search): ~0.005s (faster but less accurate)
  • semgrep (semantic analysis): ~15s (slower but more powerful)
  • ast-grep: Sweet spot between speed and accuracy

Real-World Examples

Example 1: Finding Module Options Without Types

Goal: Find all mkOption calls in custom modules that don’t specify a type.

Command:

ast-grep -p 'mkOption { $$$OPTS }' -l nix modules/ | grep -v "type ="

Results: Found 137 instances across modules:

  • modules/jellyfin-auto-collections/: 13 options
  • modules/nixpkgs-consolidate/: 8 options
  • modules/govanityurl/: 4 options
  • modules/microshift/: 4 options
  • And more…

Action: These should be fixed by adding explicit types for better documentation and type checking.

Example 2: Bash Script Safety Audit

Goal: Identify bash scripts missing error handling.

Command:

ast-grep scan --json | jq -r '.[] | select(.ruleId == "bash-require-strict-mode") | .file'

Results:

install.sh
keyboards/eyelash_corne/go.sh
imperative/nagoya/apply.sh
imperative/wakasu/apply.sh
tools/tmp/create-vm.sh
tools/tmp/install.sh
...

Fix: Add set -euo pipefail after shebang in each file.

Impact: Prevents silent failures and undefined variable bugs.

Example 3: Finding Unsafe rm -rf Usage

Goal: Audit potentially dangerous rm -rf commands.

Command:

ast-grep scan --json | jq -r '.[] | select(.ruleId == "bash-unsafe-rm-rf") | {file, line: .range.start.line, code: .text}'

Results: 5 instances found, example:

# tools/nix-flake-update/nix-flake-update.sh:103
rm -rf "$WORKTREE_DIR" || true

Analysis: In this case, it’s actually safe because:

  1. Variable is defined at script start with timestamp
  2. Cleanup is in trap handler
  3. Has || true to prevent trap failure

Learning: Not all matches are bugs - ast-grep helps you find patterns, you still need to review them.

Example 4: Searching Across Languages

Goal: Find all password-related configurations across Nix and Bash.

Nix search:

ast-grep -p 'passwordFile = $PATH' -l nix systems/

Bash search:

ast-grep -p 'PASSWORD=$VALUE' -l bash tools/

Benefit: Single tool, consistent interface, structural matching.

Advanced Usage

Interactive Refactoring

Suppose we want to standardize error logging in bash scripts:

ast-grep -p 'echo "ERROR: $MSG" >&2' \
  --rewrite 'log "ERROR: $MSG"' \
  --interactive \
  -l bash tools/

This will:

  1. Find all matching patterns
  2. Show you each match with context
  3. Ask if you want to apply the replacement
  4. Keep track of what you’ve changed

Writing Custom Rules

Create a new rule in .ast-grep/rules/my-rule.yml:

id: my-custom-rule
message: Your custom message here
severity: warning  # error, warning, info, hint
language: Nix
rule:
  pattern: $PATTERN_HERE
fix: $FIXED_PATTERN  # optional
note: |
  Detailed explanation of why this rule exists
  and how to fix violations.

Test your rule:

ast-grep scan --rule .ast-grep/rules/my-rule.yml

JSON Output for Automation

Get structured data for processing:

ast-grep scan --json | jq -r '.[] | 
  select(.severity == "error") | 
  {file, line: .range.start.line, rule: .ruleId}'

Use in CI/CD:

# Fail build if errors found
if ast-grep scan --json | jq -e '.[] | select(.severity == "error")' >/dev/null; then
  echo "ast-grep found errors!"
  exit 1
fi

Integration with Workflow

Pre-commit Hook

Add to .git/hooks/pre-commit:

#!/usr/bin/env bash
echo "Running ast-grep linting..."
if ! ast-grep scan --error-only; then
  echo "ast-grep found errors. Commit aborted."
  echo "Run 'ast-grep scan' to see details."
  exit 1
fi

Makefile Integration

Already integrated via make lint:

.PHONY: lint-ast-grep
lint-ast-grep:
	@echo "Running ast-grep linting..."
	@ast-grep scan

Editor Integration

VSCode: Install ast-grep.ast-grep-vscode extension

  • Real-time diagnostics
  • Quick fixes
  • Hover documentation

Emacs: Use LSP mode with ast-grep server

ast-grep lsp

When to Use ast-grep

Good Use Cases

Structural code search: Finding function calls, specific syntax patterns ✅ Cross-language patterns: Same tool for Nix, Bash, Go, Python ✅ Quick refactoring: Renaming, updating patterns across files ✅ Custom linting: Repository-specific conventions ✅ Code audits: Finding security issues, deprecated patterns ✅ Learning codebase: Discovering usage patterns

When NOT to Use

Deep semantic analysis: Use language-specific tools (e.g., hnix for Nix, gopls for Go) ❌ Type checking: Use proper type checkers ❌ Complex data flow: Use static analyzers like semgrep ❌ Simple text search: Use ripgrep for speed

Complementary Tools

Use ast-grep alongside:

  • statix: Nix-specific linter (semantic analysis)
  • deadnix: Unused Nix code detection
  • shellcheck: Bash linting (deeper analysis)
  • golangci-lint: Go linting suite
  • semgrep: Advanced security scanning

ast-grep fills the gap between simple text search and full semantic analysis.

Common Patterns

Find All Functions

Bash:

ast-grep -p '$NAME() { $$$ }' -l bash

Go:

ast-grep -p 'func $NAME($$$ARGS) { $$$ }' -l go

Python:

ast-grep -p 'def $NAME($$$ARGS): $$$' -l python

Find Conditional Patterns

Nix attribute sets with specific keys:

ast-grep -p '{ enable = $VAL; $$$ }' -l nix

Bash error handling:

ast-grep -p 'if [[ $? -ne 0 ]]; then $$$ fi' -l bash

Extract Structured Data

Get all module option descriptions:

ast-grep -p 'description = "$DESC"' -l nix modules/ --json | \
  jq -r '.[].text' | \
  sort -u

Roadmap

Completed ✅

  • Install ast-grep in environment
  • Create sgconfig.yml configuration
  • Write Nix linting rules
  • Write Bash safety rules
  • Write security rules
  • Document usage and examples
  • Performance benchmarking
  • Real-world examples from repository

Planned 🔄

  • Add pre-commit hook integration
  • Create more Nix best-practice rules
  • Add Python linting rules
  • Add Go error-handling rules
  • Integrate with CI/CD pipeline
  • Create Makefile targets for common tasks
  • LSP integration for real-time feedback
  • Shared rule repository for NixOS community

Future 🔮

  • Tekton-specific rules (for work projects)
  • Custom rules for home-manager patterns
  • Auto-fix automation for safe rules
  • Monthly scan reports
  • Rule effectiveness tracking

Resources

Contributing

Adding New Rules

  1. Identify a pattern that should be checked
  2. Create rule file: .ast-grep/rules/category-rule-name.yml
  3. Test: ast-grep scan --rule .ast-grep/rules/category-rule-name.yml
  4. Document in this guide
  5. Commit with message: feat(ast-grep): add rule for [pattern]

Rule Template

id: category-descriptive-name
message: Clear, actionable message
severity: error  # or warning, info, hint
language: Nix  # or Bash, Go, Python, etc.
note: |
  Detailed explanation:
  - Why this rule exists
  - How to fix violations
  - Links to documentation
rule:
  pattern: $PATTERN_HERE
fix: $FIXED_PATTERN  # optional, only if mechanical fix exists

FAQ

Q: Is ast-grep production-ready? A: Yes, version 0.40+ is stable. Used by many projects including Stripe, Uber, and others.

Q: How does it compare to semgrep? A: ast-grep is faster and simpler, semgrep has more advanced features. Use ast-grep for structural patterns, semgrep for deep security analysis.

Q: Can I use it in CI/CD? A: Yes! It’s designed for this. Fast enough to run on every commit.

Q: What about false positives? A: AST-based matching has very few false positives compared to regex. Review findings, adjust rules as needed.

Q: Can I disable rules? A: Yes, use severity: off in the rule file, or use --rule flag to run specific rules.

Q: How do I debug patterns? A: Use the playground to test patterns interactively.


Last updated: 2026-02-09
ast-grep version: 0.40.5
Repository: ~/src/home