Graph-sitter pre-computes dependencies and usages for all symbols in the codebase, enabling constant-time queries for these relationships.
Overview
Graph-sitter provides two main ways to track relationships between symbols:
Dependencies and usages are inverses of each other. For example, given the following input code:
# Input code
from module import BaseClass
class MyClass(BaseClass):
pass
The following assertions will hold in the Codegen API:
base = codebase.get_symbol("BaseClass")
my_class = codebase.get_symbol("MyClass")
# MyClass depends on BaseClass
assert base in my_class.dependencies
# BaseClass is used by MyClass
assert my_class in base.usages
If A depends on B, then B is used by A. This relationship is tracked in both directions, allowing you to navigate the codebase from either perspective.
-
MyClass.dependencies answers the question: “which symbols in the codebase does MyClass depend on?”
-
BaseClass.usages answers the question: “which symbols in the codebase use BaseClass?”
Usage Types
Both APIs use the UsageType enum to specify different kinds of relationships:
class UsageType(IntFlag):
DIRECT = auto() # Direct usage within the same file
CHAINED = auto() # Usage through attribute access (module.symbol)
INDIRECT = auto() # Usage through a non-aliased import
ALIASED = auto() # Usage through an aliased import
DIRECT Usage
A direct usage occurs when a symbol is used in the same file where it’s defined, without going through any imports or attribute access.
# Define MyClass
class MyClass:
def __init__(self):
pass
# Direct usage of MyClass in same file
class Child(MyClass):
pass
CHAINED Usage
A chained usage occurs when a symbol is accessed through module or object attribute access, using dot notation.
import module
# Chained usage of ClassB through module
obj = module.ClassB()
# Chained usage of method through obj
result = obj.method()
INDIRECT Usage
An indirect usage happens when a symbol is used through a non-aliased import statement.
from module import BaseClass
# Indirect usage of BaseClass through import
class MyClass(BaseClass):
pass
ALIASED Usage
An aliased usage occurs when a symbol is used through an import with an alias.
from module import BaseClass as AliasedBase
# Aliased usage of BaseClass
class MyClass(AliasedBase):
pass
Dependencies API
The dependencies API lets you find what symbols a given symbol depends on.
Basic Usage
# Get all direct dependencies
deps = my_class.dependencies # Shorthand for dependencies(UsageType.DIRECT)
# Get dependencies of specific types
direct_deps = my_class.dependencies(UsageType.DIRECT)
chained_deps = my_class.dependencies(UsageType.CHAINED)
indirect_deps = my_class.dependencies(UsageType.INDIRECT)
Combining Usage Types
You can combine usage types using the bitwise OR operator:
# Get both direct and indirect dependencies
deps = my_class.dependencies(UsageType.DIRECT | UsageType.INDIRECT)
# Get all types of dependencies
deps = my_class.dependencies(
UsageType.DIRECT | UsageType.CHAINED |
UsageType.INDIRECT | UsageType.ALIASED
)
Common Patterns
- Finding dead code (symbols with no usages):
# Check if a symbol is unused
def is_dead_code(symbol):
return not symbol.usages
# Find all unused functions in a file
dead_functions = [f for f in file.functions if not f.usages]
- Finding all imports that a symbol uses:
# Get all imports a class depends on
class_imports = [dep for dep in my_class.dependencies if isinstance(dep, Import)]
# Get all imports used by a function, including indirect ones
all_function_imports = [
dep for dep in my_function.dependencies(UsageType.DIRECT | UsageType.INDIRECT)
if isinstance(dep, Import)
]
Traversing the Dependency Graph
Sometimes you need to analyze not just direct dependencies, but the entire dependency graph up to a certain depth. The dependencies method allows you to traverse the dependency graph and collect all dependencies up to a specified depth level.
Basic Usage
# Get only direct dependencies
deps = symbol.dependencies(max_depth=1)
# Get deep dependencies (up to 5 levels)
deps = symbol.dependencies(max_depth=5)
The method returns a dictionary mapping each symbol to its list of direct dependencies. This makes it easy to analyze the dependency structure:
# Print the dependency tree
for sym, direct_deps in deps.items():
print(f"{sym.name} depends on: {[d.name for d in direct_deps]}")
Example: Analyzing Class Inheritance
Here’s an example of using dependencies to analyze a class inheritance chain:
class A:
def method_a(self): pass
class B(A):
def method_b(self):
self.method_a()
class C(B):
def method_c(self):
self.method_b()
# Get the full inheritance chain
symbol = codebase.get_class("C")
deps = symbol.dependencies(
max_depth=3
)
# Will show:
# C depends on: [B]
# B depends on: [A]
# A depends on: []
Handling Cyclic Dependencies
The method properly handles cyclic dependencies in the codebase:
class A:
def method_a(self):
return B()
class B:
def method_b(self):
return A()
# Get dependencies including cycles
symbol = codebase.get_class("A")
deps = symbol.dependencies()
# Will show:
# A depends on: [B]
# B depends on: [A]
The max_depth parameter helps prevent excessive recursion in large codebases or when there are cycles in the dependency graph.