Skip to content

Call Graph Analysis

geoffrey fernald edited this page Feb 1, 2026 · 3 revisions

Call Graph Analysis

Drift builds a complete call graph of your codebase, enabling powerful data flow analysis and impact assessment.

What is a Call Graph?

A call graph maps every function call in your codebase:

main()
  β”œβ”€β”€ handleRequest()
  β”‚     β”œβ”€β”€ validateInput()
  β”‚     β”œβ”€β”€ authenticate()
  β”‚     β”‚     └── verifyToken()
  β”‚     └── processData()
  β”‚           β”œβ”€β”€ fetchUser()
  β”‚           β”‚     └── db.query("SELECT * FROM users")
  β”‚           └── updateRecord()
  β”‚                 └── db.query("UPDATE users SET...")
  └── sendResponse()

This enables Drift to answer questions like:

  • "What data can this code access?"
  • "Who can access this sensitive data?"
  • "What's the blast radius if I change this function?"

Building the Call Graph

# Build call graph (happens automatically during scan)
drift scan

# Or build explicitly
drift callgraph build

# Check status
drift callgraph status

# Security-prioritized view (P0-P4 tiers)
drift callgraph status --security

Output:

Call Graph Status
=================

Files analyzed: 247
Functions extracted: 1,842
Calls mapped: 5,631
Data access points: 312

Languages:
  TypeScript: 189 files, 1,456 functions
  Python: 58 files, 386 functions

Frameworks detected:
  Express: 45 route handlers
  Prisma: 89 data access points
  FastAPI: 23 endpoints

Forward Reachability

"What data can this code access?"

Starting from a code location, trace forward to see all data it can reach:

# From a specific line
drift callgraph reach src/api/users.ts:42

# From a function
drift callgraph reach handleUserUpdate

# Limit traversal depth
drift callgraph reach src/api/users.ts:42 --max-depth 5

Example Output:

Forward Reachability from src/api/users.ts:42
=============================================

Direct Access (depth 1):
  β†’ users.email (PII)
  β†’ users.name (PII)
  β†’ users.updated_at

Via fetchUserProfile (depth 2):
  β†’ users.password_hash (SENSITIVE)
  β†’ users.phone (PII)
  β†’ sessions.token (SENSITIVE)

Via updateUserPreferences (depth 3):
  β†’ preferences.* 
  β†’ audit_log.* 

Total: 12 data points reachable
  - 4 PII fields
  - 2 SENSITIVE fields
  - 6 regular fields

MCP Tool: drift_reachability

{
  "direction": "forward",
  "location": "src/api/users.ts:42",
  "maxDepth": 10,
  "sensitiveOnly": true,
  "limit": 15
}

Inverse Reachability

"Who can access this data?"

Starting from a data point, trace backward to find all code that can access it:

# Who can access password hashes?
drift callgraph inverse users.password_hash

# Who can access any user data?
drift callgraph inverse users

# Limit depth
drift callgraph inverse users.email --max-depth 5

Example Output:

Inverse Reachability to users.password_hash
===========================================

Direct Access:
  ← src/auth/login.ts:verifyPassword (line 45)
  ← src/auth/register.ts:hashPassword (line 23)
  ← src/admin/users.ts:resetPassword (line 89)

Indirect Access (via verifyPassword):
  ← src/api/auth.controller.ts:login (line 34)
  ← src/api/auth.controller.ts:changePassword (line 78)

Entry Points:
  POST /api/auth/login
  POST /api/auth/change-password
  POST /api/admin/users/:id/reset-password

⚠️  Warning: 3 entry points can reach sensitive data

MCP Tool: drift_reachability

{
  "direction": "inverse",
  "target": "users.password_hash",
  "maxDepth": 10,
  "limit": 15
}

Impact Analysis

"What breaks if I change this?"

Before making changes, understand the blast radius:

# Analyze impact of changing a file
drift callgraph impact src/auth/login.ts

# Analyze impact of changing a function
drift callgraph impact verifyToken

# Find dead code (functions never called)
drift callgraph dead

# Analyze test coverage for sensitive data
drift callgraph coverage

Example Output:

Impact Analysis: src/auth/login.ts
==================================

Direct Callers (12):
  src/api/auth.controller.ts:login
  src/api/auth.controller.ts:refreshToken
  src/middleware/auth.ts:requireAuth
  ...

Indirect Callers (47):
  All routes using @RequireAuth middleware
  
Affected Tests (8):
  tests/auth/login.test.ts
  tests/api/auth.controller.test.ts
  tests/middleware/auth.test.ts
  tests/e2e/auth-flow.test.ts
  ...

Entry Points Affected (23):
  All authenticated API endpoints

Risk Assessment: HIGH
  - Core authentication function
  - 23 entry points depend on this
  - Recommend comprehensive testing

MCP Tool: drift_impact_analysis

{
  "target": "src/auth/login.ts",
  "maxDepth": 10,
  "limit": 10
}

Function Details

Get detailed information about a specific function:

drift callgraph function handleUserUpdate

Output:

Function: handleUserUpdate
==========================

Location: src/services/user.service.ts:45-89
Signature: async handleUserUpdate(userId: string, data: UpdateUserDTO): Promise<User>

Calls (8):
  β†’ validateUpdateData (src/validators/user.ts:12)
  β†’ fetchUser (src/repositories/user.ts:34)
  β†’ checkPermissions (src/auth/permissions.ts:56)
  β†’ updateUser (src/repositories/user.ts:78)
  β†’ invalidateCache (src/cache/user.ts:23)
  β†’ publishEvent (src/events/user.ts:45)
  β†’ logAudit (src/audit/logger.ts:12)
  β†’ sendNotification (src/notifications/user.ts:34)

Called By (3):
  ← UserController.update (src/controllers/user.controller.ts:67)
  ← AdminController.updateUser (src/controllers/admin.controller.ts:123)
  ← BatchProcessor.processUserUpdates (src/jobs/batch.ts:89)

Data Access:
  β†’ users.* (read, write)
  β†’ audit_log.* (write)
  β†’ cache:user:* (delete)

Patterns Detected:
  - Repository pattern
  - Event-driven updates
  - Audit logging

Cross-Language Analysis

Drift's call graph works across languages:

TypeScript Frontend          Python Backend
==================          ==============
fetchUsers()                 
  β†’ fetch('/api/users')  ──→  get_users()
                                β†’ db.query(users)
                                
updateUser()
  β†’ fetch('/api/users/:id') ──→ update_user()
                                  β†’ validate_input()
                                  β†’ db.update(users)

This enables:

  • API contract verification β€” Frontend calls match backend endpoints
  • Full-stack data flow β€” Trace data from UI to database
  • Cross-service impact β€” Understand microservice dependencies

How It Works

1. AST Parsing

Drift uses Tree-sitter to parse source code into ASTs:

// Source code
function handleLogin(email: string, password: string) {
  const user = await findUserByEmail(email);
  const valid = await verifyPassword(password, user.passwordHash);
  return valid ? createSession(user) : null;
}

// Extracted calls
[
  { callee: "findUserByEmail", args: ["email"], line: 2 },
  { callee: "verifyPassword", args: ["password", "user.passwordHash"], line: 3 },
  { callee: "createSession", args: ["user"], line: 4 }
]

2. Call Resolution

Drift resolves call targets across files:

handleLogin
  β†’ findUserByEmail β†’ src/repositories/user.ts:findUserByEmail
  β†’ verifyPassword β†’ src/auth/password.ts:verifyPassword
  β†’ createSession β†’ src/auth/session.ts:createSession

3. Data Access Detection

Drift detects database access patterns:

// Prisma
const user = await prisma.user.findUnique({ where: { email } });
// Detected: users.* (read)

// Raw SQL
const result = await db.query('SELECT * FROM users WHERE email = $1', [email]);
// Detected: users.* (read)

// TypeORM
const user = await userRepository.findOne({ email });
// Detected: users.* (read)

4. Graph Construction

All data is combined into a unified call graph:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Call Graph                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Nodes: Functions, Methods, Classes                              β”‚
β”‚  Edges: Calls, Data Access, Imports                              β”‚
β”‚  Metadata: Line numbers, Types, Patterns                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Storage & Performance

SQLite Storage (v1.0+)

Call graph data is stored in SQLite for optimal performance:

.drift/lake/callgraph/
└── callgraph.db          # SQLite database with WAL mode

Key benefits:

  • O(1) memory queries β€” No need to load entire graph
  • Concurrent access β€” Multiple tools can query simultaneously
  • 8x faster builds β€” Parallel parsing with batched writes
  • No OOM β€” Handles 10K+ file codebases that previously crashed

Incremental Updates

Only changed files are re-analyzed:

# Full build
drift callgraph build

# Incremental (default)
drift callgraph build --incremental

# Force full rebuild
drift callgraph build --force

Performance (v1.0 Rust Core)

Codebase Size Build Time Query Time
<10K LOC <1s <50ms
10-100K LOC 1-5s <100ms
100K-1M LOC 5-30s <500ms
>1M LOC 30s-2min <1s

Note: v1.0 introduced SQLite-backed storage and Rust-native parsing, providing 4-8x performance improvements over previous versions.


Best Practices

1. Build Before Querying

# Always scan first
drift scan

# Then query
drift callgraph reach src/api/users.ts:42

2. Use Depth Limits

For large codebases, limit traversal depth:

drift callgraph reach src/api/users.ts:42 --max-depth 5

3. Focus on Sensitive Data

Use --sensitive-only to focus on what matters:

drift callgraph inverse users.password_hash --sensitive-only

4. Combine with Impact Analysis

Before making changes:

# 1. Check what you're changing
drift callgraph function handleLogin

# 2. Check impact
drift callgraph impact handleLogin

# 3. Check test coverage for sensitive data
drift callgraph coverage

# 4. Check what tests to run
drift test-topology affected src/auth/login.ts

Troubleshooting

"No call graph data"

Run drift scan first to build the call graph.

"Function not found"

Check the function name matches exactly. Use drift callgraph status to see what's indexed.

"Slow queries"

  • Use --max-depth to limit traversal
  • Use --sensitive-only to filter results
  • Run drift callgraph build --force to rebuild

"Missing calls"

Some dynamic calls can't be statically analyzed:

  • eval() / exec()
  • Dynamic imports
  • Reflection-based calls

Drift uses regex fallback for common patterns but may miss edge cases.

Clone this wiki locally