home / skills / splitleaseteam / splitlease_monorepo / ast-dependency-analyzer

ast-dependency-analyzer skill

/.claude/skills/ast-dependency-analyzer

This skill analyzes JavaScript and TypeScript projects to build dependency graphs and identify barrels, hubs, cycles, and orphaned files.

npx playbooks add skill splitleaseteam/splitlease_monorepo --skill ast-dependency-analyzer

Review the files below or copy the command above to add this skill to your agents.

Files (5)
SKILL.md
11.0 KB
---
name: ast-dependency-analyzer
description: |
  Analyzes JavaScript/TypeScript codebases using tree-sitter AST parsing to extract exports, imports,
  and build complete dependency graphs. Use when: (1) Detecting barrel/hub files, (2) Finding circular
  dependencies, (3) Identifying orphaned files, (4) Analyzing import patterns, (5) Understanding codebase
  structure before refactoring. Returns DependencyContext with symbol_table, dependency_graph, and
  reverse_dependencies.
---

# AST Dependency Analyzer

Analyzes JavaScript/TypeScript codebases using tree-sitter AST parsing to extract exports, imports, and build complete dependency graphs.

## When to Use

Use this skill when you need to:
- **Detect barrel files** - Find files that re-export from other modules
- **Find hub files** - Identify files that many others depend on
- **Trace dependencies** - Understand what imports from what
- **Find circular dependencies** - Detect import cycles
- **Identify orphaned files** - Find files nothing imports from
- **Plan refactoring** - Understand dependency structure before changes
- **Validate imports** - Ensure all imports resolve correctly

## What It Analyzes

**Languages:** JavaScript (.js, .jsx, .mjs, .cjs) and TypeScript (.ts, .tsx)

**Extracts from each file:**
- All exports (named, default, re-exports, type exports)
- All imports (named, default, namespace, side-effect, dynamic)
- Resolved import paths
- Line numbers for each export/import

**Builds:**
- `symbol_table` - What each file exports
- `dependency_graph` - What each file imports (and from where)
- `reverse_dependencies` - Who depends on each file (reverse lookup)

## Quick Start

```bash
# Navigate to project root
cd "c:\Users\Split Lease\Documents\Split Lease - Team"

# Run analysis
python -c "
import sys
sys.path.insert(0, '.claude/skills/ast-dependency-analyzer')
from ast_dependency_analyzer import analyze_dependencies

# Analyze directory (returns DependencyContext)
context = analyze_dependencies('app/src', force_refresh=True)

# Get markdown output for prompts
print(context.to_prompt_context())
"
```

**Alternative (as package):**
```python
# If added to PYTHONPATH or installed
from ast_dependency_analyzer import analyze_dependencies, validate_file_after_write

context = analyze_dependencies("app/src")
print(context.to_prompt_context())
```

**Caching:** The analyzer caches results by date. Use `force_refresh=True` to re-analyze.

## Output Structure

### DependencyContext Object

```python
@dataclass
class DependencyContext:
    root_dir: str                              # Directory analyzed
    symbol_table: Dict[str, List[ExportedSymbol]]       # What each file exports
    dependency_graph: Dict[str, List[ImportedSymbol]]   # What each file imports
    reverse_dependencies: Dict[str, List[str]]          # Who imports from each file
    total_files: int
    total_exports: int
    total_imports: int
    analysis_timestamp: str
    parse_error_count: int
    content_hash: str                          # For cache validation
```

### ExportedSymbol

```python
@dataclass
class ExportedSymbol:
    name: str                    # The exported name
    export_type: ExportType      # "named", "default", "re-export", "declaration", "type"
    line: int                    # Line number (1-based)
    source_file: Optional[str]   # For re-exports, the source module
    original_name: Optional[str] # For aliased exports
```

### ImportedSymbol

```python
@dataclass
class ImportedSymbol:
    name: str                    # The imported symbol name (or '*' for namespace)
    import_type: ImportType      # "named", "default", "namespace", "side-effect", "dynamic", "type"
    source: str                  # The module specifier as written
    resolved_path: Optional[str] # Resolved absolute path (None for node_modules)
    line: int                    # Line number (1-based)
    alias: Optional[str]         # For aliased imports
```

## Common Analysis Patterns

### Find Barrel Files (Re-exporters)

Barrel files have `export_type = "re-export"`:

```python
barrels = []
for file_path, exports in context.symbol_table.items():
    re_exports = [e for e in exports if e.export_type == "re-export"]
    if re_exports:
        barrels.append({
            'file': file_path,
            're_export_count': len(re_exports),
            'is_pure': all(e.export_type == "re-export" for e in exports),
            'consumers': len(context.reverse_dependencies.get(file_path, []))
        })
```

### Find Hub Files (Highly Depended Upon)

```python
hubs = []
for file_path, dependents in context.reverse_dependencies.items():
    if len(dependents) >= 10:  # Threshold for "hub"
        hubs.append({
            'file': file_path,
            'consumer_count': len(dependents),
            'consumers': dependents
        })

# Sort by consumer count (descending)
hubs.sort(key=lambda x: x['consumer_count'], reverse=True)
```

### Find Orphaned Files (Nothing Imports From)

```python
orphans = []
for file_path in context.symbol_table.keys():
    dependents = context.reverse_dependencies.get(file_path, [])
    if len(dependents) == 0:
        orphans.append(file_path)
```

### Find Circular Dependencies

```python
def find_cycles(context, start_file, visited=None, path=None):
    if visited is None:
        visited = set()
    if path is None:
        path = []

    if start_file in path:
        cycle_start = path.index(start_file)
        return path[cycle_start:]

    if start_file in visited:
        return None

    visited.add(start_file)
    path.append(start_file)

    # Get dependencies
    imports = context.dependency_graph.get(start_file, [])
    for imp in imports:
        if imp.resolved_path:
            cycle = find_cycles(context, imp.resolved_path, visited, path)
            if cycle:
                return cycle

    path.pop()
    return None

# Check all files for cycles
all_cycles = []
for file_path in context.symbol_table.keys():
    cycle = find_cycles(context, file_path)
    if cycle:
        all_cycles.append(cycle)
```

### Get All Consumers of a File

```python
def get_consumers(context, file_path):
    """Get all files that import from the given file."""
    return context.reverse_dependencies.get(file_path, [])

# Example:
consumers = get_consumers(context, 'app/src/lib/utils.js')
print(f"utils.js is imported by {len(consumers)} files")
```

### Get All Dependencies of a File

```python
def get_dependencies(context, file_path):
    """Get all files that the given file imports from."""
    imports = context.dependency_graph.get(file_path, [])
    return [imp.resolved_path for imp in imports if imp.resolved_path]

# Example:
deps = get_dependencies(context, 'app/src/pages/HomePage.jsx')
print(f"HomePage.jsx imports from {len(deps)} files")
```

## Export Types Reference

| Type | Example | Indicates |
|------|---------|-----------|
| `re-export` | `export { foo } from './bar'` | **Barrel file** - re-exports from another module |
| `named` | `export { foo, bar }` | Local exports defined in this file |
| `default` | `export default function()` | Local default export |
| `declaration` | `export const foo = ...` | Local export declaration |
| `type` | `export type { Foo }` | TypeScript type-only export |

**Barrel Detection:**
- **Pure barrel**: All exports are `re-export` type
- **Mixed barrel**: Has `re-export` AND other export types
- **Not a barrel**: No `re-export` exports

## Import Types Reference

| Type | Example | Description |
|------|---------|-------------|
| `named` | `import { foo } from './mod'` | Named import |
| `default` | `import foo from './mod'` | Default import |
| `namespace` | `import * as foo from './mod'` | Namespace import |
| `side-effect` | `import './styles.css'` | Side-effect import (no bindings) |
| `dynamic` | `await import('./mod')` | Dynamic import |
| `type` | `import type { Foo }` | TypeScript type-only import |

## API Reference

### analyze_dependencies()

Main entry point for analysis.

```python
analyze_dependencies(
    target_path: str,           # Directory to analyze (e.g., "app/src")
    cache_dir: Optional[str] = None,     # Override default cache location
    force_refresh: bool = False,         # Bypass cache and re-analyze
    max_age_hours: int = 24             # Maximum cache age
) -> DependencyContext
```

**Caching behavior:**
- Creates cache file: `ast_cache/<dirname>-<YYMMDD>.json`
- Reuses cache if file contents unchanged (via content hash)
- Keeps last 3 days of cache automatically

### DependencyContext Methods

```python
# Get files that depend on this file
context.get_dependents(file_path: str) -> List[str]

# Get files this file depends on
context.get_dependencies(file_path: str) -> List[str]

# Get exported symbol names
context.get_exports(file_path: str) -> List[str]

# Format as markdown for prompts
context.to_prompt_context() -> str

# Convert to JSON for storage
context.to_json_dict() -> dict
```

## Example: Complete Barrel Detection

```python
import sys
sys.path.insert(0, '.claude/skills/ast-dependency-analyzer')
from ast_dependency_analyzer import analyze_dependencies

# Analyze
context = analyze_dependencies('app/src', force_refresh=True)

# Find barrels
barrels = []
for file_path, exports in context.symbol_table.items():
    re_exports = [e for e in exports if e.export_type.value == "re-export"]

    if re_exports:
        other_exports = [e for e in exports if e.export_type.value != "re-export"]
        consumers = context.reverse_dependencies.get(file_path, [])

        barrels.append({
            'file': file_path,
            're_export_count': len(re_exports),
            'has_star_export': any(e.name == '*' for e in re_exports),
            'is_pure': len(other_exports) == 0,
            'consumer_count': len(consumers),
            'severity': 'high' if len(consumers) >= 20 or any(e.name == '*' for e in re_exports) else 'medium' if len(consumers) >= 10 else 'low'
        })

# Print results
for barrel in sorted(barrels, key=lambda x: x['consumer_count'], reverse=True):
    print(f"{barrel['file']}: {barrel['consumer_count']} consumers, {barrel['severity']} severity")
```

## Use Cases by Task

| Task | What to Analyze | Key Fields |
|------|-----------------|------------|
| **Barrel detection** | `symbol_table` for `export_type="re-export"` | `export_type`, `reverse_dependencies` |
| **Hub detection** | `reverse_dependencies` for high counts | `reverse_dependencies` |
| **Orphan detection** | `reverse_dependencies` for zero counts | `reverse_dependencies` |
| **Circular deps** | `dependency_graph` for cycles | `dependency_graph` |
| **Impact analysis** | `reverse_dependencies` before changes | `reverse_dependencies` |
| **Unused code** | `symbol_table` vs `reverse_dependencies` | Both |

## Notes

- **Parse errors**: Check `context.parse_error_count` - files with syntax errors are skipped
- **Node modules**: External packages (not starting with `.` or `@/`) have `resolved_path = None`
- **Type imports**: Included in analysis but may not affect runtime behavior
- **Cache location**: Defaults to `ast_cache/` relative to the analyzer module (`.claude/skills/ast-dependency-analyzer/ast_cache/`)

Overview

This skill analyzes JavaScript and TypeScript codebases to extract exports, imports, and build a complete dependency graph. It uses tree-sitter AST parsing to produce a DependencyContext containing a symbol table, dependency graph, and reverse dependencies. Use it to get a precise, line-numbered map of how modules relate across a project. The output is optimized for programmatic queries and prompt-friendly summaries.

How this skill works

The analyzer parses every .js/.jsx/.mjs/.cjs/.ts/.tsx file with tree-sitter and extracts all export and import nodes including re-exports, type-only imports, dynamic imports, and side-effect imports. It resolves module specifiers where possible to absolute paths and records line numbers, export/import kinds, aliases, and source files. From that data it builds three primary structures: symbol_table (what each file exports), dependency_graph (what each file imports), and reverse_dependencies (who imports each file). Results are cached by content hash and timestamp to speed repeated runs.

When to use it

  • Detect barrel files that re-export symbols from other modules
  • Find hub files that many modules depend on before refactoring
  • Detect circular dependencies and import cycles
  • Discover orphaned files that nothing imports
  • Analyze import patterns and type vs runtime imports for cleanup

Best practices

  • Run analysis from the repository root to get consistent resolved paths
  • Use force_refresh when you changed many files to bypass cache
  • Combine reverse_dependencies with symbol_table to identify unused exports
  • Treat type-only imports separately when making runtime dependency changes
  • Exclude generated or build artifacts from the target path to reduce noise

Example use cases

  • Barrel detection: find pure and mixed re-export files and rank by consumer count
  • Hub detection: identify files with many dependents to prioritize stabilization before changes
  • Circular dependency tracing: produce a cycle path for remediation and tests
  • Orphan detection: list files with zero consumers for possible deletion
  • Impact analysis: given a file, list all consumers to assess risk before edits

FAQ

What languages and file extensions are supported?

JavaScript and TypeScript: .js, .jsx, .mjs, .cjs, .ts, .tsx.

How does the analyzer handle node_modules or external packages?

External packages (non-relative specifiers) are recorded with resolved_path = null; they are tracked as imports but not resolved into your repo graph.