diff --git a/docs/building-with-codegen/dependencies-and-usages.mdx b/docs/building-with-codegen/dependencies-and-usages.mdx index 252fafe57..ab9bd3acd 100644 --- a/docs/building-with-codegen/dependencies-and-usages.mdx +++ b/docs/building-with-codegen/dependencies-and-usages.mdx @@ -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: @@ -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 @@ -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 ) @@ -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] +``` + + + The `max_depth` parameter helps prevent excessive recursion in large codebases or when there are cycles in the dependency graph. + diff --git a/src/codegen/sdk/core/interfaces/importable.py b/src/codegen/sdk/core/interfaces/importable.py index 237606640..6a57f75e7 100644 --- a/src/codegen/sdk/core/interfaces/importable.py +++ b/src/codegen/sdk/core/interfaces/importable.py @@ -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 @@ -40,31 +41,40 @@ def __init__(self, ts_node: TSNode, file_node_id: NodeId, G: "CodebaseGraph", pa 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 = [] 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))) + + 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) + 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) diff --git a/tests/unit/codegen/sdk/core/interfaces/test_importable_dependencies.py b/tests/unit/codegen/sdk/core/interfaces/test_importable_dependencies.py new file mode 100644 index 000000000..ad86cbc70 --- /dev/null +++ b/tests/unit/codegen/sdk/core/interfaces/test_importable_dependencies.py @@ -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 diff --git a/tests/unit/codegen/sdk/python/class_definition/test_class_dependencies.py b/tests/unit/codegen/sdk/python/class_definition/test_class_dependencies.py index b027f000f..bd358a65c 100644 --- a/tests/unit/codegen/sdk/python/class_definition/test_class_dependencies.py +++ b/tests/unit/codegen/sdk/python/class_definition/test_class_dependencies.py @@ -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 @@ -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) @@ -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