Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/building-with-codegen/dependencies-and-usages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,82 @@ all_function_imports = [
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 `get_symbol_dependencies` method allows you to traverse the dependency graph and collect all dependencies up to a specified depth level.

### Basic Usage

```python
# Get dependencies up to depth 2 (default)
deps = codebase.get_symbol_dependencies(symbol)

# Get only direct dependencies
deps = codebase.get_symbol_dependencies(symbol, max_depth=1)

# Get deep dependencies (up to 5 levels)
deps = codebase.get_symbol_dependencies(symbol, 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:

```python
# 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 `get_symbol_dependencies` to analyze a class inheritance chain:

```python
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
deps = codebase.get_symbol_dependencies(
codebase.get_class("C"),
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:

```python
class A:
def method_a(self):
return B()

class B:
def method_b(self):
return A()

# Get dependencies including cycles
deps = codebase.get_symbol_dependencies(
codebase.get_class("A")
)

# Will show:
# A depends on: [B]
# B depends on: [A]
```

<Tip>
The `max_depth` parameter helps prevent excessive recursion in large codebases or when there are cycles in the dependency graph.
</Tip>
71 changes: 53 additions & 18 deletions src/codegen/sdk/core/interfaces/importable.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from tree_sitter import Node as TSNode

from codegen.sdk._proxy import proxy_property
from codegen.sdk.core.autocommit import reader
from codegen.sdk.core.dataclasses.usage import UsageType
from codegen.sdk.core.expressions.expression import Expression
Expand Down Expand Up @@ -40,32 +41,66 @@
if self.file:
self.file._nodes.append(self)

@property
@proxy_property
@reader(cache=False)
def dependencies(self) -> list[Union["Symbol", "Import"]]:
def dependencies(self, usage_types: UsageType | None = None, max_depth: int | None = None) -> list[Union["Symbol", "Import"]]:
"""Returns a list of symbols that this symbol depends on.

Returns a list of symbols (including imports) that this symbol directly depends on.
The returned list is sorted by file location for consistent ordering.
Args:
usage_types (UsageType | None): The types of dependencies to search for. Defaults to UsageType.DIRECT.
max_depth (int | None): Maximum depth to traverse in the dependency graph. If provided, will recursively collect
dependencies up to this depth. Defaults to None (only direct dependencies).

Returns:
list[Union[Symbol, Import]]: A list of symbols and imports that this symbol directly depends on,
list[Union[Symbol, Import]]: A list of symbols and imports that this symbol depends on,
sorted by file location.
"""
return self.get_dependencies(UsageType.DIRECT)

@reader(cache=False)
@noapidoc
def get_dependencies(self, usage_types: UsageType) -> list[Union["Symbol", "Import"]]:
"""Returns Symbols and Importsthat this symbol depends on.

Opposite of `usages`
Note:
This method can be called as both a property or a method. If used as a property, it is equivalent to invoking it without arguments.
"""
avoid = set(self.descendant_symbols)
deps = []
for symbol in self.descendant_symbols:
deps += filter(lambda x: x not in avoid, symbol._get_dependencies(usage_types))
return sort_editables(deps, by_file=True)
if usage_types is None:
usage_types = UsageType.DIRECT

if max_depth is None:
# Standard implementation for direct dependencies
avoid = set(self.descendant_symbols)
deps = []

Check failure on line 67 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Need type annotation for "deps" (hint: "deps: list[<type>] = ...") [var-annotated]
for symbol in self.descendant_symbols:
deps += filter(lambda x: x not in avoid, symbol._get_dependencies(usage_types))
return sort_editables(deps, by_file=True)
else:
# Recursive implementation for max_depth
dependency_map: dict[Self, list[Union[Symbol, Import]]] = {}

def _collect_dependencies(current_symbol: Self, current_depth: int) -> None:
# Get direct dependencies
from codegen.sdk.core.symbol import Symbol

direct_deps = [dep for dep in current_symbol.dependencies(usage_types=usage_types) if isinstance(dep, Symbol)]

Check failure on line 79 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Missing positional argument "self" in call to "__call__" of "ProxyProperty" [call-arg]

Check failure on line 79 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Missing positional argument "self" in call to "__call__" of "ProxyProperty" [call-arg]

# Add current symbol and its dependencies to the map if not already present
# or if present but with empty dependencies (from max depth)
if current_symbol not in dependency_map or not dependency_map[current_symbol]:
dependency_map[current_symbol] = direct_deps

Check failure on line 84 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible types in assignment (expression has type "list[Symbol[Any, Any]]", target has type "list[Symbol[Any, Any] | Import[Any]]") [assignment]

Check failure on line 84 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible types in assignment (expression has type "list[Symbol[Any, Any]]", target has type "list[Symbol[Any, Any] | Import[Any]]") [assignment]

# Process dependencies if not at max depth
if current_depth < max_depth:

Check failure on line 87 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Unsupported operand types for < ("int" and "None") [operator]
for dep in direct_deps:
_collect_dependencies(dep, current_depth + 1)
else:
# At max depth, ensure dependencies are in map with empty lists
for dep in direct_deps:
if dep not in dependency_map:
dependency_map[dep] = []

Check failure on line 94 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Invalid index type "Symbol[Any, Any]" for "dict[Self, list[Symbol[Any, Any] | Import[Any]]]"; expected type "Self" [index]

Check failure on line 94 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Invalid index type "Symbol[Any, Any]" for "dict[Self, list[Symbol[Any, Any] | Import[Any]]]"; expected type "Self" [index]

# Start recursive collection from depth 1
_collect_dependencies(self, 1)

# Return all unique dependencies found
all_deps = set()
for deps in dependency_map.values():
all_deps.update(deps)
return sort_editables(list(all_deps), by_file=True)

@reader(cache=False)
@noapidoc
Expand All @@ -78,7 +113,7 @@
edges = [x for x in self.G.out_edges(self.node_id) if x[2].type == EdgeType.SYMBOL_USAGE]
unique_dependencies = []
for edge in edges:
if edge[2].usage.usage_type is None or edge[2].usage.usage_type in usage_types:

Check failure on line 116 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Item "None" of "Usage | None" has no attribute "usage_type" [union-attr]
dependency = self.G.get_node(edge[1])
unique_dependencies.append(dependency)
return sort_editables(unique_dependencies, by_file=True)
Expand All @@ -94,7 +129,7 @@
if incremental:
self._remove_internal_edges(EdgeType.SYMBOL_USAGE)
try:
self._compute_dependencies()

Check failure on line 132 in src/codegen/sdk/core/interfaces/importable.py

View workflow job for this annotation

GitHub Actions / mypy

error: Missing positional argument "usage_type" in call to "_compute_dependencies" of "Editable" [call-arg]
except Exception as e:
logger.exception(f"Error in file {self.file.path} while computing dependencies for symbol {self.name}")
raise e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,18 +250,18 @@ class ClassB:
method = local_class.get_method("method")

# Test DIRECT dependencies (RenamedClass and file2)
direct_deps = method.get_dependencies(UsageType.DIRECT)
direct_deps = method.dependencies(UsageType.DIRECT)
assert len(direct_deps) == 2
assert any(dep.name == "RenamedClass" for dep in direct_deps)
assert any(dep.name == "file2" for dep in direct_deps)

# Test CHAINED dependencies (ClassB accessed through file2)
chained_deps = method.get_dependencies(UsageType.CHAINED)
chained_deps = method.dependencies(UsageType.CHAINED)
assert len(chained_deps) == 1
assert any(dep.name == "ClassB" for dep in chained_deps)

# Test combined DIRECT | CHAINED
all_deps = method.get_dependencies(UsageType.DIRECT | UsageType.CHAINED)
all_deps = method.dependencies(UsageType.DIRECT | UsageType.CHAINED)
assert len(all_deps) == 3


Expand All @@ -288,14 +288,14 @@ class HelperClass:
my_class = file1.get_class("MyClass")

# Test class DIRECT dependencies (both base class and helper used in method)
direct_deps = my_class.get_dependencies(UsageType.DIRECT)
direct_deps = my_class.dependencies(UsageType.DIRECT)
assert len(direct_deps) == 2 # Both AliasedBase and AliasedHelper
assert any(dep.name == "AliasedBase" for dep in direct_deps)
assert any(dep.name == "AliasedHelper" for dep in direct_deps)

# Test method dependencies (only helper used directly in method)
method = my_class.get_method("method")
method_deps = method.get_dependencies(UsageType.DIRECT)
method_deps = method.dependencies(UsageType.DIRECT)
assert len(method_deps) == 1 # Just AliasedHelper
assert any(dep.name == "AliasedHelper" for dep in method_deps)

Expand All @@ -322,14 +322,14 @@ class BaseClass:
my_class = file1.get_class("MyClass")

# Test MyClass dependencies
my_class_deps = my_class.get_dependencies(UsageType.INDIRECT)
my_class_deps = my_class.dependencies(UsageType.INDIRECT)
assert len(my_class_deps) == 1 # BaseClass through import
assert any(dep.name == "BaseClass" for dep in my_class_deps)

# Test AnotherClass dependencies
direct_deps = another_class.get_dependencies(UsageType.DIRECT)
direct_deps = another_class.dependencies(UsageType.DIRECT)
assert len(direct_deps) == 1 # MyClass in same file
assert any(dep.name == "MyClass" for dep in direct_deps)

indirect_deps = another_class.get_dependencies(UsageType.INDIRECT)
indirect_deps = another_class.dependencies(UsageType.INDIRECT)
assert len(indirect_deps) == 0 # BaseClass through import
Loading