Skip to main content

Overview

When debugging complex errors, understanding file relationships is crucial. Splat’s dependency graph system uses AST parsing to build a complete map of import relationships, ensuring the LLM sees all relevant context—not just the file where the error surfaced.

Why Dependency Graphs Matter

Consider this error scenario:
main.py
from utils import calculate

result = calculate(42)  # TypeError: calculate() missing 1 required argument
utils.py
def calculate(x, y):  # Function signature was recently changed
    return x + y
Without seeing utils.py, an LLM might suggest fixing the call site when the real issue is the function signature change. Dependency graphs solve this.

Graph Construction

Adjacency List Structure

Splat builds a directed graph where each file points to its imports:
{
  "/path/to/main.py": ["/path/to/utils.py", "/path/to/config.py"],
  "/path/to/utils.py": ["/path/to/helpers.py"],
  "/path/to/helpers.py": []
}

Build Process

The build_adjacency_list function recursively processes files:
utils/utils.py
def build_adjacency_list(files: List[str], project_root: str) -> Dict[str, List[str]]:
    adjacency_list = {}
    processed_files = set()

    def process_file(file: str):
        if file in processed_files or not is_project_file(file, project_root):
            return

        processed_files.add(file)
        imports = set()
        tree = None

        try:
            with open(file, 'r') as f:
                content = f.read()
                try:
                    tree = ast.parse(content)
                    for node in ast.walk(tree):
                        if isinstance(node, ast.Import):
                            imports.update(alias.name for alias in node.names)
                        elif isinstance(node, ast.ImportFrom) and node.module:
                            imports.add(node.module)
                except SyntaxError:
                    pass  # Skip files with syntax errors
        except FileNotFoundError:
            return
1

Read File

Open the Python file and read its contents
2

Parse AST

Use Python’s ast module to build an abstract syntax tree
3

Extract Imports

Walk the AST and collect all import statements
4

Resolve Paths

Convert module names to file paths within the project
5

Recurse

Repeat for each imported file

AST-Based Import Extraction

Supported Import Styles

Splat handles all Python import patterns:
import utils
# AST node: ast.Import
# Extracts: "utils"

AST Walking

The code uses ast.walk() to traverse the entire syntax tree:
for node in ast.walk(tree):
    if isinstance(node, ast.Import):
        # import x, y, z
        imports.update(alias.name for alias in node.names)
    elif isinstance(node, ast.ImportFrom) and node.module:
        # from x import y
        imports.add(node.module)
ast.walk() performs a depth-first traversal, visiting every node including nested imports within functions or classes.

Path Resolution

Module to File Mapping

Once module names are extracted, Splat resolves them to file paths:
utils/utils.py
for imp in imports:
    module_paths = []
    if '.' in imp:
        # Nested module: utils.helpers → utils/helpers.py
        module_paths.append(os.path.join(project_root, *imp.split('.')) + '.py')
    else:
        # Check relative to current file, then project root
        module_paths.extend([
            os.path.join(file_dir, f"{imp}.py"),
            os.path.join(project_root, f"{imp}.py")
        ])

    for module_path in module_paths:
        if os.path.exists(module_path):
            adjacency_list[file].append(module_path)
            process_file(module_path)  # Recursive call
            break

Resolution Strategy

1

Nested Modules

Convert dotted paths: utils.helpersutils/helpers.py
2

Relative Imports

Check same directory as importing file first
3

Project Root

Fall back to checking project root directory
4

Validation

Only add paths that exist on the filesystem
Standard library imports (like os, sys) won’t resolve to project files and are safely ignored.

Graph Traversal

Once the graph is built, Splat traverses it to find all related files:
utils/utils.py
def get_nth_related_files(start_files: List[str], graph: Dict[str, List[str]]) -> Set[str]:
  related_files = set(start_files)
  planned_visit = list(start_files)
  possible_files = set()

  while planned_visit:
    current = planned_visit.pop(0)  # BFS: process queue front
    possible_files.add(current)

    for neighbor in graph.get(current, []):
      if neighbor not in related_files:
        related_files.add(neighbor)
        planned_visit.append(neighbor)

  return possible_files
The traversal uses BFS to explore the graph:
graph LR A[main.py] —> B[utils.py] A —> C[config.py] B —> D[helpers.py] B —> E[validators.py] D —> F[constants.py] style A fill:#ff6b6b style B fill:#ffd93d style C fill:#ffd93d style D fill:#6bcf7f style E fill:#6bcf7f style F fill:#4ecdc4

Start Files

Begin with error trace files (red)

First Degree

Add direct imports (yellow)

Second Degree

Add imports of imports (green)

Nth Degree

Continue until all relationships exhausted (blue)
BFS ensures closer relationships are processed first, which can be useful for prioritizing context if token limits are hit.

Example: Complete Graph Build

Let’s trace a full example:

Input Files

main.py
from utils import calculate
from config import settings

result = calculate(settings.value)  # Error occurs here
utils.py
from helpers import validate

def calculate(x):
    return validate(x) * 2
helpers.py
from constants import MAX_VALUE

def validate(x):
    return min(x, MAX_VALUE)

Graph Output

{
  "/project/main.py": ["/project/utils.py", "/project/config.py"],
  "/project/utils.py": ["/project/helpers.py"],
  "/project/helpers.py": ["/project/constants.py"],
  "/project/config.py": [],
  "/project/constants.py": []
}

Traversal Result

Starting from main.py (error file):
get_nth_related_files(["/project/main.py"], graph)
# Returns:
{
  "/project/main.py",      # Start file
  "/project/utils.py",     # Direct import
  "/project/config.py",    # Direct import
  "/project/helpers.py",   # 2nd degree
  "/project/constants.py"  # 3rd degree
}
All 5 files will be included in the context sent to the LLM, providing complete visibility into the error chain.

Project Boundary Enforcement

Filtering External Imports

Splat only processes files within your project:
utils/utils.py
def is_project_file(file_path: str, project_root: str) -> bool:
  return os.path.commonpath([file_path, project_root]) == project_root
def process_file(file: str):
    if file in processed_files or not is_project_file(file, project_root):
        return  # Skip if already processed or outside project
Without this check, Splat would try to process standard library files like /usr/lib/python3.9/os.py, which would be wasteful and slow.

Error Handling

Graceful Degradation

The graph builder handles various failure modes:
Files with syntax errors (the likely cause of the original error) are still added to the graph, but their imports are skipped:
try:
    tree = ast.parse(content)
except SyntaxError:
    pass  # Skip import extraction, but file is still in graph
Import references to non-existent files are silently skipped:
for module_path in module_paths:
    if os.path.exists(module_path):
        adjacency_list[file].append(module_path)
        break  # Stop searching once found
The processed_files set prevents infinite loops:
if file in processed_files:
    return  # Already processed, skip
processed_files.add(file)

Usage in Splat

Relational Mode (-r Flag)

The dependency graph is activated with the -r flag:
relational.py
if flag == '-r':
  graph = build_adjacency_list(collected_traceback_files, project_root)
  all_related_files = get_nth_related_files(collected_traceback_files, graph)
  return traceback, error_information, run_mock_repopack(list(all_related_files))
else:
  return traceback, error_information, run_mock_repopack(collected_traceback_files)
Only error trace files:
splat python main.py
# Context: [main.py]

Performance Considerations

Caching

The processed_files set ensures each file is parsed only once, even if imported by multiple files.

Lazy Evaluation

Files are processed only when needed during traversal, avoiding upfront cost.

AST Over Regex

AST parsing is more robust than regex for extracting imports, handling edge cases correctly.

Boundary Checks

Project boundary filtering prevents processing hundreds of stdlib files.

Limitations

Dynamic Imports: The current implementation doesn’t handle importlib or __import__() calls, which require runtime analysis.Conditional Imports: Imports inside if blocks are always extracted, even if the condition is false at runtime.Relative Imports: Currently only supports absolute imports and simple relative imports (.module), not complex relative paths (..parent.module).

Next Steps

Git Awareness

Learn how Splat integrates with Git for context

Error Analysis

See how dependency context improves LLM analysis

Build docs developers (and LLMs) love