Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dependencies): unified dependencies… #362

Merged
Show file tree
Hide file tree
Changes from all 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
92 changes: 84 additions & 8 deletions docs/building-with-codegen/dependencies-and-usages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Codegen pre-computes dependencies and usages for all symbols in the codebase, en

Codegen provides two main ways to track relationships between symbols:

- [.dependencies](/api-reference/core/Symbol#dependencies) / [.get_dependencies(...)](/api-reference/core/Symbol#get-dependencies) - What symbols does this symbol depend on?
- [.dependencies](/api-reference/core/Symbol#dependencies) / - What symbols does this symbol depend on?
- [.usages](/api-reference/core/Symbol#usages) / [.usages(...)](/api-reference/core/Symbol#usages) - Where is this symbol used?

Dependencies and usages are inverses of each other. For example, given the following input code:
Expand Down Expand Up @@ -129,12 +129,12 @@ The dependencies API lets you find what symbols a given symbol depends on.

```python
# Get all direct dependencies
deps = my_class.dependencies # Shorthand for get_dependencies(UsageType.DIRECT)
deps = my_class.dependencies # Shorthand for dependencies(UsageType.DIRECT)

# Get dependencies of specific types
direct_deps = my_class.get_dependencies(UsageType.DIRECT)
chained_deps = my_class.get_dependencies(UsageType.CHAINED)
indirect_deps = my_class.get_dependencies(UsageType.INDIRECT)
direct_deps = my_class.dependencies(UsageType.DIRECT)
chained_deps = my_class.dependencies(UsageType.CHAINED)
indirect_deps = my_class.dependencies(UsageType.INDIRECT)
```

### Combining Usage Types
Expand All @@ -143,10 +143,10 @@ You can combine usage types using the bitwise OR operator:

```python
# Get both direct and indirect dependencies
deps = my_class.get_dependencies(UsageType.DIRECT | UsageType.INDIRECT)
deps = my_class.dependencies(UsageType.DIRECT | UsageType.INDIRECT)

# Get all types of dependencies
deps = my_class.get_dependencies(
deps = my_class.dependencies(
UsageType.DIRECT | UsageType.CHAINED |
UsageType.INDIRECT | UsageType.ALIASED
)
Expand Down Expand Up @@ -178,7 +178,83 @@ 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.get_dependencies(UsageType.DIRECT | UsageType.INDIRECT)
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

```python

# 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:

```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 `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
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:

```python
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]
```

<Tip>
The `max_depth` parameter helps prevent excessive recursion in large codebases or when there are cycles in the dependency graph.
</Tip>
38 changes: 24 additions & 14 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,31 +41,40 @@
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 = UsageType.DIRECT, 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.
"""
# Get direct dependencies for this symbol and its descendants
avoid = set(self.descendant_symbols)
deps = []

Check failure on line 63 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))
deps.extend(filter(lambda x: x not in avoid, symbol._get_dependencies(usage_types)))

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

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[Symbol[Any, Any] | Import[Any]], TypeGuard[Never]]" [arg-type]

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

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[Symbol[Any, Any] | Import[Any]], TypeGuard[Any]]" [arg-type]

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

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "_get_dependencies" of "Importable" has incompatible type "UsageType | None"; expected "UsageType" [arg-type]

if max_depth is not None and max_depth > 1:
# For max_depth > 1, recursively collect dependencies
seen = set(deps)
for dep in list(deps): # Create a copy of deps to iterate over
if isinstance(dep, Importable):
next_deps = dep.dependencies(usage_types=usage_types, max_depth=max_depth - 1)

Check failure on line 72 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]
for next_dep in next_deps:
if next_dep not in seen:
seen.add(next_dep)
deps.append(next_dep)

return sort_editables(deps, by_file=True)

@reader(cache=False)
Expand All @@ -78,7 +88,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 91 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,12 +104,12 @@
if incremental:
self._remove_internal_edges(EdgeType.SYMBOL_USAGE)
try:
self._compute_dependencies()

Check failure on line 107 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
if incremental:
return self.descendant_symbols + self.file.get_nodes(sort=False)

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

View workflow job for this annotation

GitHub Actions / mypy

error: No overload variant of "__add__" of "list" matches argument type "Sequence[Importable[Any]]" [operator]
return []

@commiter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
from codegen.sdk.codebase.factory.get_session import get_codebase_session
from codegen.sdk.core.dataclasses.usage import UsageType
from codegen.sdk.enums import ProgrammingLanguage


def test_dependencies_max_depth_python(tmpdir) -> None:
"""Test the max_depth parameter in dependencies property for Python."""
# language=python
content = """
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()

def use_c():
c = C()
c.method_c()
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}) as codebase:
file = codebase.get_file("test.py")
use_c = file.get_function("use_c")
c_class = file.get_class("C")
b_class = file.get_class("B")
a_class = file.get_class("A")

# Test depth 1 (direct dependencies only)
deps_depth1 = use_c.dependencies(max_depth=1)
assert len(deps_depth1) == 1
assert deps_depth1[0] == c_class

# Test depth 2 (includes C's dependency on B)
deps_depth2 = use_c.dependencies(max_depth=2)
assert len(deps_depth2) == 2
assert c_class in deps_depth2
assert b_class in deps_depth2

# Test depth 3 (includes full chain use_c -> C -> B -> A)
deps_depth3 = use_c.dependencies(max_depth=3)
assert len(deps_depth3) == 3
assert c_class in deps_depth3
assert b_class in deps_depth3
assert a_class in deps_depth3

# Test with both max_depth and usage_types
deps_with_types = use_c.dependencies(max_depth=2, usage_types=UsageType.DIRECT)
assert len(deps_with_types) == 2
assert c_class in deps_with_types
assert b_class in deps_with_types


def test_dependencies_max_depth_typescript(tmpdir) -> None:
"""Test the max_depth parameter in dependencies property for TypeScript."""
# language=typescript
content = """
interface IBase {
baseMethod(): void;
}

class A implements IBase {
baseMethod() {
console.log('base');
}
}

class B extends A {
methodB() {
this.baseMethod();
}
}

class C extends B {
methodC() {
this.methodB();
}
}

function useC() {
const c = new C();
c.methodC();
}
"""
with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={"test.ts": content}) as codebase:
file = codebase.get_file("test.ts")
use_c = file.get_function("useC")
c_class = file.get_class("C")
b_class = file.get_class("B")
a_class = file.get_class("A")
ibase = file.get_interface("IBase")

# Test depth 1 (direct dependencies only)
deps_depth1 = use_c.dependencies(max_depth=1)
assert len(deps_depth1) == 1
assert deps_depth1[0] == c_class

# Test depth 2 (includes C's dependency on B)
deps_depth2 = use_c.dependencies(max_depth=2)
assert len(deps_depth2) == 2
assert c_class in deps_depth2
assert b_class in deps_depth2

# Test depth 3 (includes C -> B -> A)
deps_depth3 = use_c.dependencies(max_depth=3)
assert len(deps_depth3) == 3
assert c_class in deps_depth3
assert b_class in deps_depth3
assert a_class in deps_depth3

# Test depth 4 (includes interface implementation)
deps_depth4 = use_c.dependencies(max_depth=4)
assert len(deps_depth4) == 4
assert c_class in deps_depth4
assert b_class in deps_depth4
assert a_class in deps_depth4
assert ibase in deps_depth4

# Test with both max_depth and usage_types
deps_with_types = use_c.dependencies(max_depth=2)
assert len(deps_with_types) == 2
assert c_class in deps_with_types
assert b_class in deps_with_types


def test_dependencies_max_depth_cyclic(tmpdir) -> None:
"""Test max_depth parameter with cyclic dependencies."""
# language=python
content = """
class A:
def method_a(self):
return B()

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

def use_both():
a = A()
b = B()
return a.method_a(), b.method_b()
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}) as codebase:
file = codebase.get_file("test.py")
use_both = file.get_function("use_both")
a_class = file.get_class("A")
b_class = file.get_class("B")

# Test depth 1 (direct dependencies only)
deps_depth1 = use_both.dependencies(max_depth=1)
assert len(deps_depth1) == 2
assert a_class in deps_depth1
assert b_class in deps_depth1

# Test depth 2 (should handle cyclic deps without infinite recursion)
deps_depth2 = use_both.dependencies(max_depth=2)
assert len(deps_depth2) == 2 # Still just A and B due to cycle
assert a_class in deps_depth2
assert b_class in deps_depth2

# Test with both max_depth and usage_types
deps_with_types = use_both.dependencies(max_depth=2)
assert len(deps_with_types) == 2
assert a_class in deps_with_types
assert b_class in deps_with_types
Loading