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.
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
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.
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
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
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.
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.
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).