Skip to content

Commit bca3332

Browse files
tawsifkamalcodegen-bot
and
codegen-bot
committed
Tawsif add support for codebase exports (#117)
# Motivation <!-- Why is this change necessary? --> - adds exports to codebase.exports and directory.exports + directory.get_export(name) for typescript codebases - Made a new PR so that I didn't have to deal with merge conflicts from graphsitter renaming to codegen-sdk - Fixed all comments from previous PR - Added better typehints to ensure that exports are meant only for typescript codebases and typescript specific directories --------- Co-authored-by: codegen-bot <[email protected]>
1 parent 193c4d7 commit bca3332

File tree

7 files changed

+251
-6
lines changed

7 files changed

+251
-6
lines changed

src/codegen/sdk/core/codebase.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from codegen.sdk.core.detached_symbols.code_block import CodeBlock
4040
from codegen.sdk.core.detached_symbols.parameter import Parameter
4141
from codegen.sdk.core.directory import Directory
42+
from codegen.sdk.core.export import Export
4243
from codegen.sdk.core.external_module import ExternalModule
4344
from codegen.sdk.core.file import File, SourceFile
4445
from codegen.sdk.core.function import Function
@@ -59,19 +60,22 @@
5960
from codegen.sdk.python.file import PyFile
6061
from codegen.sdk.python.function import PyFunction
6162
from codegen.sdk.python.import_resolution import PyImport
63+
from codegen.sdk.python.statements.import_statement import PyImportStatement
6264
from codegen.sdk.python.symbol import PySymbol
6365
from codegen.sdk.typescript.assignment import TSAssignment
6466
from codegen.sdk.typescript.class_definition import TSClass
6567
from codegen.sdk.typescript.detached_symbols.code_block import TSCodeBlock
6668
from codegen.sdk.typescript.detached_symbols.parameter import TSParameter
69+
from codegen.sdk.typescript.export import TSExport
6770
from codegen.sdk.typescript.file import TSFile
6871
from codegen.sdk.typescript.function import TSFunction
6972
from codegen.sdk.typescript.import_resolution import TSImport
7073
from codegen.sdk.typescript.interface import TSInterface
74+
from codegen.sdk.typescript.statements.import_statement import TSImportStatement
7175
from codegen.sdk.typescript.symbol import TSSymbol
7276
from codegen.sdk.typescript.type_alias import TSTypeAlias
7377
from codegen.sdk.utils import determine_project_language
74-
from codegen.shared.decorators.docs import apidoc, noapidoc
78+
from codegen.shared.decorators.docs import apidoc, noapidoc, py_noapidoc
7579
from codegen.shared.exceptions.control_flow import MaxAIRequestsError
7680
from codegen.shared.performance.stopwatch_utils import stopwatch
7781
from codegen.visualizations.visualization_manager import VisualizationManager
@@ -91,6 +95,11 @@
9195
TTypeAlias = TypeVar("TTypeAlias", bound="TypeAlias")
9296
TParameter = TypeVar("TParameter", bound="Parameter")
9397
TCodeBlock = TypeVar("TCodeBlock", bound="CodeBlock")
98+
TExport = TypeVar("TExport", bound="Export")
99+
TSGlobalVar = TypeVar("TSGlobalVar", bound="Assignment")
100+
PyGlobalVar = TypeVar("PyGlobalVar", bound="Assignment")
101+
TSDirectory = Directory[TSFile, TSSymbol, TSImportStatement, TSGlobalVar, TSClass, TSFunction, TSImport]
102+
PyDirectory = Directory[PyFile, PySymbol, PyImportStatement, PyGlobalVar, PyClass, PyFunction, PyImport]
94103

95104

96105
@apidoc
@@ -263,6 +272,27 @@ def imports(self) -> list[TImport]:
263272
"""
264273
return self.G.get_nodes(NodeType.IMPORT)
265274

275+
@property
276+
@py_noapidoc
277+
def exports(self: "TSCodebaseType") -> list[TSExport]:
278+
"""Returns a list of all Export nodes in the codebase.
279+
280+
Retrieves all Export nodes from the codebase graph. These exports represent all export statements across all files in the codebase,
281+
including exports from both internal modules and external packages. This is a TypeScript-only codebase property.
282+
283+
Args:
284+
None
285+
286+
Returns:
287+
list[TSExport]: A list of Export nodes representing all exports in the codebase.
288+
TExport can only be a TSExport for TypeScript codebases.
289+
290+
"""
291+
if self.language == ProgrammingLanguage.PYTHON:
292+
raise NotImplementedError("Exports are not supported for Python codebases since Python does not have an export mechanism.")
293+
294+
return self.G.get_nodes(NodeType.EXPORT)
295+
266296
@property
267297
def external_modules(self) -> list[ExternalModule]:
268298
"""Returns a list of all external modules in the codebase.
@@ -1145,5 +1175,5 @@ def from_repo(cls, repo_name: str, *, tmp_dir: str | None = None, commit: str |
11451175
# The last 2 lines of code are added to the runner. See codegen-backend/cli/generate/utils.py
11461176
# Type Aliases
11471177
CodebaseType = Codebase[SourceFile, Directory, Symbol, Class, Function, Import, Assignment, Interface, TypeAlias, Parameter, CodeBlock]
1148-
PyCodebaseType = Codebase[PyFile, Directory, PySymbol, PyClass, PyFunction, PyImport, PyAssignment, Interface, TypeAlias, PyParameter, PyCodeBlock]
1149-
TSCodebaseType = Codebase[TSFile, Directory, TSSymbol, TSClass, TSFunction, TSImport, TSAssignment, TSInterface, TSTypeAlias, TSParameter, TSCodeBlock]
1178+
PyCodebaseType = Codebase[PyFile, PyDirectory, PySymbol, PyClass, PyFunction, PyImport, PyAssignment, Interface, TypeAlias, PyParameter, PyCodeBlock]
1179+
TSCodebaseType = Codebase[TSFile, TSDirectory, TSSymbol, TSClass, TSFunction, TSImport, TSAssignment, TSInterface, TSTypeAlias, TSParameter, TSCodeBlock]

src/codegen/sdk/core/directory.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33
from pathlib import Path
44
from typing import TYPE_CHECKING, Generic, Self, TypeVar
55

6-
from codegen.shared.decorators.docs import apidoc
6+
from codegen.shared.decorators.docs import apidoc, py_noapidoc
77

88
if TYPE_CHECKING:
99
from codegen.sdk.core.assignment import Assignment
1010
from codegen.sdk.core.class_definition import Class
1111
from codegen.sdk.core.file import File
1212
from codegen.sdk.core.function import Function
13-
from codegen.sdk.core.import_resolution import ImportStatement
13+
from codegen.sdk.core.import_resolution import Import, ImportStatement
1414
from codegen.sdk.core.symbol import Symbol
15+
from codegen.sdk.typescript.class_definition import TSClass
16+
from codegen.sdk.typescript.export import TSExport
17+
from codegen.sdk.typescript.file import TSFile
18+
from codegen.sdk.typescript.function import TSFunction
19+
from codegen.sdk.typescript.import_resolution import TSImport
20+
from codegen.sdk.typescript.statements.import_statement import TSImportStatement
21+
from codegen.sdk.typescript.symbol import TSSymbol
1522

1623
import logging
1724

@@ -24,10 +31,13 @@
2431
TGlobalVar = TypeVar("TGlobalVar", bound="Assignment")
2532
TClass = TypeVar("TClass", bound="Class")
2633
TFunction = TypeVar("TFunction", bound="Function")
34+
TImport = TypeVar("TImport", bound="Import")
35+
36+
TSGlobalVar = TypeVar("TSGlobalVar", bound="Assignment")
2737

2838

2939
@apidoc
30-
class Directory(Generic[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction]):
40+
class Directory(Generic[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction, TImport]):
3141
"""Directory representation for codebase.
3242
GraphSitter abstraction of a file directory that can be used to look for files and symbols within a specific directory.
3343
"""
@@ -133,6 +143,17 @@ def functions(self) -> list[TFunction]:
133143
"""Get a recursive list of all functions in the directory and its subdirectories."""
134144
return list(chain.from_iterable(f.functions for f in self.files))
135145

146+
@property
147+
@py_noapidoc
148+
def exports(self: "Directory[TSFile, TSSymbol, TSImportStatement, TSGlobalVar, TSClass, TSFunction, TSImport]") -> "list[TSExport]":
149+
"""Get a recursive list of all exports in the directory and its subdirectories."""
150+
return list(chain.from_iterable(f.exports for f in self.files))
151+
152+
@property
153+
def imports(self) -> list[TImport]:
154+
"""Get a recursive list of all imports in the directory and its subdirectories."""
155+
return list(chain.from_iterable(f.imports for f in self.files))
156+
136157
def get_symbol(self, name: str) -> TSymbol | None:
137158
"""Get a symbol by name in the directory and its subdirectories."""
138159
return next((s for s in self.symbols if s.name == name), None)
@@ -176,6 +197,15 @@ def get_file(self, filename: str, ignore_case: bool = False) -> TFile | None:
176197
return next((f for name, f in self.items.items() if name.lower() == filename.lower() and isinstance(f, File)), None)
177198
return self.items.get(filename, None)
178199

200+
@py_noapidoc
201+
def get_export(self: "Directory[TSFile, TSSymbol, TSImportStatement, TSGlobalVar, TSClass, TSFunction, TSImport]", name: str) -> "TSExport | None":
202+
"""Get an export by name in the directory and its subdirectories (supports only typescript)."""
203+
return next((s for s in self.exports if s.name == name), None)
204+
205+
def get_import(self, name: str) -> TImport | None:
206+
"""Get an import by name in the directory and its subdirectories."""
207+
return next((s for s in self.imports if s.name == name), None)
208+
179209
def add_subdirectory(self, subdirectory: Self) -> None:
180210
"""Add a subdirectory to the directory."""
181211
rel_path = os.path.relpath(subdirectory.dirpath, self.dirpath)

src/codegen/shared/decorators/docs.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ def noapidoc(obj: T) -> T:
7070
return obj
7171

7272

73+
py_no_apidoc_objects: list[DocumentedObject] = []
74+
py_no_apidoc_signatures: set[str] = set()
75+
76+
77+
def py_noapidoc(obj: T) -> T:
78+
"""Decorator for things that are hidden from the Python API documentation for AI-agent prompts."""
79+
obj._py_apidoc = False
80+
obj._api_doc_lang = "python"
81+
if doc_obj := get_documented_object(obj):
82+
bisect.insort(py_no_apidoc_objects, doc_obj)
83+
py_no_apidoc_signatures.add(doc_obj.signature())
84+
return obj
85+
86+
7387
def get_documented_object(obj) -> DocumentedObject | None:
7488
module = inspect.getmodule(obj)
7589
module_name = module.__name__ if module else ""
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
3+
from codegen.sdk.codebase.factory.get_session import get_codebase_session
4+
from codegen.sdk.enums import ProgrammingLanguage
5+
6+
7+
def test_python_exports_not_supported(tmpdir):
8+
"""Test that exports are not supported in Python codebases."""
9+
# language=python
10+
content = """
11+
def hello():
12+
pass
13+
"""
14+
# Create a Python codebase with a simple Python file
15+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
16+
# Verify that accessing exports raises NotImplementedError
17+
with pytest.raises(NotImplementedError):
18+
_ = codebase.exports
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from codegen.sdk.codebase.factory.get_session import get_codebase_session
2+
from codegen.sdk.enums import ProgrammingLanguage
3+
4+
5+
def test_codebase_exports(tmpdir) -> None:
6+
# language=typescript
7+
content = """
8+
export const a = 1;
9+
export let b = 2;
10+
export var c = 3;
11+
export function foo() {}
12+
export class Bar {}
13+
export interface IFoo {}
14+
export type MyType = string;
15+
export { foo as default };
16+
"""
17+
with get_codebase_session(tmpdir=tmpdir, files={"file.ts": content}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase:
18+
assert len(codebase.exports) == 8
19+
export_names = {exp.name for exp in codebase.exports}
20+
assert export_names == {"a", "b", "c", "foo", "Bar", "IFoo", "MyType", "default"}
21+
22+
23+
def test_codebase_reexports(tmpdir) -> None:
24+
# language=typescript
25+
content1 = """
26+
export const x = 1;
27+
export const y = 2;
28+
"""
29+
content2 = """
30+
export { x } from './file1';
31+
export { y as z } from './file1';
32+
"""
33+
with get_codebase_session(tmpdir=tmpdir, files={"file1.ts": content1, "file2.ts": content2}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase:
34+
assert len(codebase.exports) == 4
35+
export_names = {exp.name for exp in codebase.exports}
36+
assert export_names == {"x", "y", "z"}
37+
38+
39+
def test_codebase_default_exports(tmpdir) -> None:
40+
# language=typescript
41+
content = """
42+
const value = 42;
43+
export default value;
44+
"""
45+
with get_codebase_session(tmpdir=tmpdir, files={"file.ts": content}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase:
46+
assert len(codebase.exports) == 1
47+
export = codebase.exports[0]
48+
assert export.name == "value"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from codegen.sdk.codebase.factory.get_session import get_codebase_session
2+
from codegen.sdk.enums import ProgrammingLanguage
3+
4+
5+
def test_codebase_exports(tmpdir) -> None:
6+
# language=typescript
7+
content = """
8+
export const a = 1;
9+
export let b = 2;
10+
export var c = 3;
11+
export function foo() {}
12+
export class Bar {}
13+
export interface IFoo {}
14+
export type MyType = string;
15+
export { foo as default };
16+
"""
17+
with get_codebase_session(tmpdir=tmpdir, files={"file.ts": content}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase:
18+
assert len(codebase.exports) == 8
19+
export_names = {exp.name for exp in codebase.exports}
20+
assert export_names == {"a", "b", "c", "foo", "Bar", "IFoo", "MyType", "default"}
21+
22+
23+
def test_codebase_reexports(tmpdir) -> None:
24+
# language=typescript
25+
content1 = """
26+
export const x = 1;
27+
export const y = 2;
28+
"""
29+
content2 = """
30+
export { x } from './file1';
31+
export { y as z } from './file1';
32+
"""
33+
with get_codebase_session(tmpdir=tmpdir, files={"file1.ts": content1, "file2.ts": content2}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase:
34+
assert len(codebase.exports) == 4
35+
export_names = {exp.name for exp in codebase.exports}
36+
assert export_names == {"x", "y", "z"}
37+
38+
39+
def test_codebase_default_exports(tmpdir) -> None:
40+
# language=typescript
41+
content = """
42+
const value = 42;
43+
export default value;
44+
"""
45+
with get_codebase_session(tmpdir=tmpdir, files={"file.ts": content}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase:
46+
assert len(codebase.exports) == 1
47+
export = codebase.exports[0]
48+
assert export.name == "value"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from codegen.sdk.codebase.factory.get_session import get_codebase_session
2+
from codegen.sdk.enums import ProgrammingLanguage
3+
4+
5+
def test_directory_imports(tmpdir) -> None:
6+
# language=typescript
7+
content1 = """
8+
import { a, b } from '../shared';
9+
import type { IFoo } from './types';
10+
"""
11+
content2 = """
12+
import { c } from '../shared';
13+
import defaultExport from './module';
14+
"""
15+
with get_codebase_session(
16+
tmpdir=tmpdir, files={"dir1/file1.ts": content1, "dir1/file2.ts": content2, "dir2/file3.ts": "import { d } from '../shared';"}, programming_language=ProgrammingLanguage.TYPESCRIPT
17+
) as codebase:
18+
dir1 = codebase.get_directory("dir1")
19+
dir2 = codebase.get_directory("dir2")
20+
21+
# Test dir1 imports
22+
assert len(dir1.imports) == 5
23+
dir1_import_names = {imp.name for imp in dir1.imports}
24+
assert dir1_import_names == {"a", "b", "IFoo", "c", "defaultExport"}
25+
26+
# Test dir2 imports
27+
assert len(dir2.imports) == 1
28+
assert dir2.imports[0].name == "d"
29+
30+
# Test get_import method
31+
assert dir1.get_import("a") is not None
32+
assert dir1.get_import("d") is None
33+
assert dir2.get_import("d") is not None
34+
35+
36+
def test_directory_nested_imports(tmpdir) -> None:
37+
# language=typescript
38+
content1 = """
39+
import { a } from './module1';
40+
"""
41+
content2 = """
42+
import { b } from '../module2';
43+
"""
44+
content3 = """
45+
import { c } from '../../module3';
46+
"""
47+
with get_codebase_session(
48+
tmpdir=tmpdir, files={"dir1/file1.ts": content1, "dir1/subdir/file2.ts": content2, "dir1/subdir/deepdir/file3.ts": content3}, programming_language=ProgrammingLanguage.TYPESCRIPT
49+
) as codebase:
50+
dir1 = codebase.get_directory("dir1")
51+
subdir = codebase.get_directory("dir1/subdir")
52+
deepdir = codebase.get_directory("dir1/subdir/deepdir")
53+
54+
# Test imports at each directory level
55+
assert len(dir1.imports) == 3 # Should include all nested imports
56+
assert len(subdir.imports) == 2 # Should include its own and deeper imports
57+
assert len(deepdir.imports) == 1 # Should only include its own imports

0 commit comments

Comments
 (0)