From a65955d4c12547e0470e4dd6d6b97bd0e43b12c5 Mon Sep 17 00:00:00 2001 From: tkucar Date: Thu, 23 Jan 2025 22:30:51 +0100 Subject: [PATCH 01/13] old changes --- .../typescript/import_resolution.py | 29 +++ src/graph_sitter/typescript/namespace.py | 204 +++++++++++++++++- .../typescript/namespace/test_namespace.py | 3 +- .../test_namespace_complex_examples.py | 39 ++++ .../namespace/test_namespace_modifications.py | 137 ++++++++++++ .../namespace/test_namespace_usage.py | 104 +++++++++ 6 files changed, 503 insertions(+), 13 deletions(-) create mode 100644 tests/unit/typescript/namespace/test_namespace_modifications.py create mode 100644 tests/unit/typescript/namespace/test_namespace_usage.py diff --git a/src/graph_sitter/typescript/import_resolution.py b/src/graph_sitter/typescript/import_resolution.py index 12c68dca6..42713fe9c 100644 --- a/src/graph_sitter/typescript/import_resolution.py +++ b/src/graph_sitter/typescript/import_resolution.py @@ -23,6 +23,7 @@ if TYPE_CHECKING: from graph_sitter.typescript.file import TSFile + from graph_sitter.typescript.namespace import TSNamespace from graph_sitter.typescript.statements.import_statement import TSImportStatement @@ -565,6 +566,34 @@ def names(self) -> Generator[tuple[str, Self | WildcardImport[Self]], None, None return yield from super().names + @property + def namespace_imports(self) -> list[TSNamespace]: + """Returns any namespace objects imported by this import statement. + + For example: + import * as MyNS from './mymodule'; + + Returns: + List of namespace objects imported + """ + if not self.is_namespace_import(): + return [] + + resolved = self.resolved_symbol + if resolved is None or not isinstance(resolved, TSNamespace): + return [] + + return [resolved] + + def is_namespace_import(self) -> bool: + """Returns True if this import is importing a namespace. + + Examples: + import * as MyNS from './mymodule'; # True + import { foo } from './mymodule'; # False + """ + return self.import_type == ImportType.NAMESPACE and self.alias is not None + @override def set_import_module(self, new_module: str) -> None: """Sets the module of an import. diff --git a/src/graph_sitter/typescript/namespace.py b/src/graph_sitter/typescript/namespace.py index ae3116872..db8dd7d1b 100644 --- a/src/graph_sitter/typescript/namespace.py +++ b/src/graph_sitter/typescript/namespace.py @@ -6,13 +6,12 @@ from graph_sitter.codebase.codebase_graph import CodebaseGraph from graph_sitter.core.autocommit import commiter -from graph_sitter.core.dataclasses.usage import UsageKind +from graph_sitter.core.dataclasses.usage import Usage, UsageKind, UsageType from graph_sitter.core.interfaces.has_name import HasName from graph_sitter.core.node_id_factory import NodeId from graph_sitter.core.statements.statement import Statement -from graph_sitter.core.statements.symbol_statement import SymbolStatement from graph_sitter.core.symbol import Symbol -from graph_sitter.enums import SymbolType +from graph_sitter.enums import EdgeType, SymbolType from graph_sitter.extensions.utils import cached_property from graph_sitter.typescript.class_definition import TSClass from graph_sitter.typescript.enum_definition import TSEnum @@ -51,7 +50,19 @@ def _compute_dependencies(self, usage_type: UsageKind | None = None, dest: HasNa # Use self as destination if none provided dest = dest or self.self_dest - # Compute dependencies from the namespace's code block + if dest and dest != self: + # Add direct usage of namespace itself + usage = Usage(kind=usage_type, match=self, usage_type=UsageType.DIRECT, usage_symbol=dest.parent_symbol, imported_by=None) + self.G.add_edge(self.node_id, dest.node_id, EdgeType.SYMBOL_USAGE, usage) + + # For each exported symbol accessed through namespace + for symbol in self.symbols: + if symbol and symbol.ts_node_type == "export_statement": + # Add chained usage edge + chained_usage = Usage(kind=usage_type, match=symbol, usage_type=UsageType.CHAINED, usage_symbol=dest.parent_symbol, imported_by=None) + self.G.add_edge(symbol.node_id, dest.node_id, EdgeType.SYMBOL_USAGE, chained_usage) + + # Compute dependencies from namespace's code block self.code_block._compute_dependencies(usage_type, dest) @cached_property @@ -59,33 +70,48 @@ def symbols(self) -> list[Symbol]: """Returns all symbols defined within this namespace, including nested ones.""" all_symbols = [] for stmt in self.code_block.statements: - # Handle export statements if stmt.ts_node_type == "export_statement": for export in stmt.exports: all_symbols.append(export.declared_symbol) - # Handle direct symbols - elif isinstance(stmt, SymbolStatement): + elif hasattr(stmt, "assignments"): + all_symbols.extend(stmt.assignments) + else: all_symbols.append(stmt) return all_symbols - def get_symbol(self, name: str, recursive: bool = True) -> Symbol | None: - """Get a symbol by name from this namespace. + def get_symbol(self, name: str, recursive: bool = True, get_private: bool = False) -> Symbol | None: + """Get an exported or private symbol by name from this namespace. Returns only exported symbols by default. Args: name: Name of the symbol to find recursive: If True, also search in nested namespaces + get_private: If True, also search in private symbols Returns: Symbol | None: The found symbol, or None if not found """ # First check direct symbols in this namespace for symbol in self.symbols: + # Handle TSAssignmentStatement case + if hasattr(symbol, "assignments"): + for assignment in symbol.assignments: + if assignment.name == name: + # If we are looking for private symbols then return it, else only return exported symbols + if get_private: + return assignment + elif assignment.is_exported: + return assignment + + # Handle regular symbol case if hasattr(symbol, "name") and symbol.name == name: - return symbol + if get_private: + return symbol + elif symbol.is_exported: + return symbol # If recursive and this is a namespace, check its symbols if recursive and isinstance(symbol, TSNamespace): - nested_symbol = symbol.get_symbol(name, recursive=True) + nested_symbol = symbol.get_symbol(name, recursive=True, get_private=get_private) return nested_symbol return None @@ -201,3 +227,159 @@ def get_nested_namespaces(self) -> list[TSNamespace]: nested.append(symbol) nested.extend(symbol.get_nested_namespaces()) return nested + + @ts_apidoc + @commiter + def add_symbol(self, symbol: TSSymbol | str, export: bool = False, remove_original: bool = False) -> TSSymbol: + """Adds a new symbol to the namespace. + + Args: + symbol: The symbol to add to the namespace (either a TSSymbol instance or source code string) + export: Whether to export the symbol. Defaults to False. + remove_original: Whether to remove the original symbol. Defaults to False. + + Returns: + The added symbol + """ + # TODO: Do we need to check if symbol can be added to the namespace? + # if not self.symbol_can_be_added(symbol): + # raise ValueError(f"Symbol {symbol.name} cannot be added to the namespace.") + # TODO: add symbol by moving + # TODO: use self.export_symbol() to export the symbol if needed ? + + print("SYMBOL to be added: ", symbol, "export: ", export) + symbol_name = symbol.name if isinstance(symbol, TSSymbol) else symbol.split(" ")[2 if export else 1] + + # Check if the symbol already exists in file + existing_symbol = self.get_symbol(symbol_name) + if existing_symbol is not None: + return existing_symbol + + # Export symbol if needed, then append to code block + if isinstance(symbol, str): + if export and not symbol.startswith("export "): + symbol = f"export {symbol}" + elif isinstance(symbol, TSSymbol): + if export and not symbol.is_exported: + export_src = f"export {symbol.source};" + self.code_block.statements.append(export_src) + self.code_block.statements.append(symbol) + self.G.commit_transactions() + + # Remove symbol from original location if remove_original is True + if remove_original and symbol.parent is not None: + symbol.parent.remove_symbol(symbol_name) + + self.G.commit_transactions() + added_symbol = self.get_symbol(symbol_name) + if added_symbol is None: + raise ValueError(f"Failed to add symbol {symbol_name} to namespace") + return added_symbol + + @ts_apidoc + @commiter + def remove_symbol(self, symbol_name: str) -> TSSymbol | None: + """Removes a symbol from the namespace by name. + + Args: + symbol_name: Name of the symbol to remove + + Returns: + The removed symbol if found, None otherwise + """ + symbol = self.get_symbol(symbol_name) + if symbol: + # Remove from code block statements + for i, stmt in enumerate(self.code_block.statements): + if symbol.source == stmt.source: + print("stmt to be removed: ", stmt) + self.code_block.statements.pop(i) + self.G.commit_transactions() + return symbol + return None + + @ts_apidoc + @commiter + def rename_symbol(self, old_name: str, new_name: str) -> None: + """Renames a symbol within the namespace. + + Args: + old_name: Current symbol name + new_name: New symbol name + """ + symbol = self.get_symbol(old_name) + if symbol: + symbol.rename(new_name) + self.G.commit_transactions() + + @commiter + def export_symbol(self, name: str) -> None: + """Marks a symbol as exported in the namespace. + + Args: + name: Name of symbol to export + """ + symbol = self.get_symbol(name) + if not symbol or symbol.is_exported: + return + + export_source = f"export {symbol.source}" + symbol.parent.edit(export_source) + self.G.commit_transactions() + + @property + def valid_import_names(self) -> set[str]: + """Returns set of valid import names for this namespace. + + This includes all exported symbols plus the namespace name itself + for namespace imports. + """ + names = {self.name} # Namespace itself can be imported + for stmt in self.code_block.statements: + if stmt.ts_node_type == "export_statement": + for export in stmt.exports: + names.add(export.name) + return names + + def resolve_import(self, import_name: str) -> Symbol | None: + """Resolves an import name to a symbol within this namespace. + + Args: + import_name: Name to resolve + + Returns: + Resolved symbol or None if not found + """ + # First check direct symbols + for symbol in self.symbols: + if symbol.is_exported and symbol.name == import_name: + return symbol + + # Then check nested namespaces + for nested in self.get_nested_namespaces(): + resolved = nested.resolve_import(import_name) + if resolved is not None: + return resolved + + return None + + @ts_apidoc + def resolve_attribute(self, name: str) -> Symbol | None: + """Resolves an attribute access on the namespace. + + Args: + name: Name of the attribute to resolve + + Returns: + The resolved symbol or None if not found + """ + # First check direct symbols + if symbol := self.get_symbol(name): + return symbol + + # Then check nested namespaces recursively + for nested in self.get_nested_namespaces(): + if symbol := nested.get_symbol(name): + return symbol + + return None diff --git a/tests/unit/typescript/namespace/test_namespace.py b/tests/unit/typescript/namespace/test_namespace.py index 0140f7ee7..f1038e218 100644 --- a/tests/unit/typescript/namespace/test_namespace.py +++ b/tests/unit/typescript/namespace/test_namespace.py @@ -65,8 +65,7 @@ def test_namespace_basic_symbols(tmpdir) -> None: assert namespace.get_symbol("privateVar") is None # private not accessible # Test symbols collection - assert len(namespace.symbols) == 2 # only exported symbols - assert all(symbol.is_exported for symbol in namespace.symbols) + assert len(namespace.symbols) == 3 def test_namespace_recursive_symbol_lookup(tmpdir) -> None: diff --git a/tests/unit/typescript/namespace/test_namespace_complex_examples.py b/tests/unit/typescript/namespace/test_namespace_complex_examples.py index 0bb3eff6d..1afd049d8 100644 --- a/tests/unit/typescript/namespace/test_namespace_complex_examples.py +++ b/tests/unit/typescript/namespace/test_namespace_complex_examples.py @@ -129,3 +129,42 @@ def test_namespace_validators(tmpdir) -> None: # Verify non-exported items are not accessible assert namespace.get_symbol("lettersRegexp") is None assert namespace.get_symbol("numberRegexp") is None + + +def test_namespace_wildcard_import(tmpdir) -> None: + """Test wildcard imports with namespaces.""" + FILE_NAME_1 = "utils.ts" + # language=typescript + FILE_CONTENT_1 = """ + export namespace Utils { + export const helper1 = () => "help1"; + export const helper2 = () => "help2"; + const internal = () => "internal"; + } + """ + + FILE_NAME_2 = "app.ts" + # language=typescript + FILE_CONTENT_2 = """ + import * as AllUtils from './utils'; + + function test() { + console.log(AllUtils.Utils.helper1()); + console.log(AllUtils.Utils.helper2()); + } + """ + + with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={FILE_NAME_1: FILE_CONTENT_1, FILE_NAME_2: FILE_CONTENT_2}) as codebase: + utils_file = codebase.get_file(FILE_NAME_1) + app_file = codebase.get_file(FILE_NAME_2) + + # Verify namespace import + utils_import = app_file.get_import("AllUtils") + assert utils_import is not None + assert utils_import.namespace == "AllUtils" + + # Verify access to exported symbols + utils_ns = utils_file.get_symbol("Utils") + assert "helper1" in utils_ns.valid_import_names + assert "helper2" in utils_ns.valid_import_names + assert "internal" not in utils_ns.valid_import_names diff --git a/tests/unit/typescript/namespace/test_namespace_modifications.py b/tests/unit/typescript/namespace/test_namespace_modifications.py new file mode 100644 index 000000000..e289af5ef --- /dev/null +++ b/tests/unit/typescript/namespace/test_namespace_modifications.py @@ -0,0 +1,137 @@ +from graph_sitter.codebase.factory.get_session import get_codebase_session +from graph_sitter.enums import ProgrammingLanguage +from graph_sitter.typescript.namespace import TSNamespace + + +def test_namespace_add_symbol(tmpdir) -> None: + """Test adding symbols to namespace.""" + FILE_NAME = "test.ts" + # language=typescript + FILE_CONTENT = """ + namespace MyNamespace { + export const x = 1; + } + """ + with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={FILE_NAME: FILE_CONTENT}) as codebase: + file = codebase.get_file("test.ts") + namespace: TSNamespace = codebase.get_symbol("MyNamespace") + + # 1. a) Add new symbol from object, then manually remove the original symbol from the file + # 1. b) Add new symbol by moving operation + file.add_symbol_from_source(source="const ya = 2") + file.add_symbol_from_source(source="const yb = 2") + codebase.G.commit_transactions() + new_const = file.get_symbol("ya") + + # Store original location + original_parent = new_const.parent + + # Add to namespace and remove from original location + namespace.add_symbol(new_const, export=True) + original_parent.remove_symbol(new_const.name) + + # Add to namespace by moving operation + # namespace.add_symbol(new_const, export=True, move=True) + + codebase.G.commit_transactions() + + # Get fresh reference to namespace + namespace: TSNamespace = codebase.get_symbol("MyNamespace") + + # Verify symbols were moved correctly + assert namespace.get_symbol("ya") is not None + assert file.get_symbol("ya") is None # Should no longer exist in file directly + # assert namespace.get_symbol("yb") is not None + # assert file.get_symbol("yb") is None # Should no longer exist in file directly + + # 2. Add new exported symbol from string + exported_code = "const z = 3" + exported = namespace.add_symbol(exported_code, export=True) + + # Verify exported symbol + assert exported is not None + assert exported.name == "z" + assert exported.parent.ts_node_type == "export_statement" + + assert len(namespace.symbols) == 2 + assert {s.name for s in namespace.symbols} == {"x", "z"} + + +def test_namespace_remove_symbol(tmpdir) -> None: + """Test removing symbols from namespace.""" + FILE_NAME = "test.ts" + # language=typescript + FILE_CONTENT = """ + namespace MyNamespace { + export const x = 1; + export const y = 2; + } + """ + with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={FILE_NAME: FILE_CONTENT}) as codebase: + namespace: TSNamespace = codebase.get_symbol("MyNamespace") + + # Remove existing symbol + removed = namespace.remove_symbol("x") + assert removed is not None + assert removed.name == "x" + + # Verify symbol was removed + assert namespace.get_symbol("x") is None + assert len(namespace.symbols) == 1 + assert namespace.symbols[0].name == "y" + + # Try removing non-existent symbol + assert namespace.remove_symbol("z") is None + + +def test_namespace_rename(tmpdir) -> None: + """Test renaming namespace.""" + FILE_NAME = "test.ts" + # language=typescript + FILE_CONTENT = """ + namespace OldName { + export const x = 1; + } + """ + with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={FILE_NAME: FILE_CONTENT}) as codebase: + namespace: TSNamespace = codebase.get_symbol("OldName") + + # Rename namespace + namespace.rename("NewName") + codebase.G.commit_transactions() + + # Verify rename + namespace: TSNamespace = codebase.get_symbol("NewName") + assert namespace.name == "NewName" + assert codebase.get_symbol("NewName") is namespace + assert codebase.get_symbol("OldName", optional=True) is None + + +def test_namespace_export_symbol(tmpdir) -> None: + """Test exporting symbols in namespace.""" + FILE_NAME = "test.ts" + # language=typescript + FILE_CONTENT = """ + namespace ExportTest { + export const external = 123; + const internal = 123; + } + """ + with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={FILE_NAME: FILE_CONTENT}) as codebase: + namespace: TSNamespace = codebase.get_symbol("ExportTest") + + # Export internal symbol + namespace.export_symbol("internal") + + # Verify export + namespace: TSNamespace = codebase.get_symbol("ExportTest") + internal = namespace.get_symbol("internal") + assert internal is not None + assert all(symbol.is_exported for symbol in namespace.symbols) + + # Export already exported symbol (no change) + namespace.export_symbol("external") + namespace: TSNamespace = codebase.get_symbol("ExportTest") + external = namespace.get_symbol("external") + assert external is not None + assert external.is_exported diff --git a/tests/unit/typescript/namespace/test_namespace_usage.py b/tests/unit/typescript/namespace/test_namespace_usage.py new file mode 100644 index 000000000..dbcdb5ef5 --- /dev/null +++ b/tests/unit/typescript/namespace/test_namespace_usage.py @@ -0,0 +1,104 @@ +from graph_sitter.codebase.factory.get_session import get_codebase_session +from graph_sitter.core.dataclasses.usage import UsageType +from graph_sitter.enums import ProgrammingLanguage + + +def test_namespace_same_file_usage(tmpdir) -> None: + """Test namespace usage within the same file.""" + # language=typescript + content = """ + namespace MathUtils { + export const PI = 3.14159; + export function square(x: number) { return x * x; } + } + + function calculateArea(radius: number) { + return MathUtils.PI * MathUtils.square(radius); + } + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.ts": content}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase: + file = codebase.get_file("test.ts") + + namespace = file.get_symbol("MathUtils") + pi = namespace.get_symbol("PI") + square = namespace.get_symbol("square") + calc_area = file.get_function("calculateArea") + + # Check if namespace is in valid_import_names + assert "MathUtils" in file.valid_symbol_names + assert "MathUtils" in namespace.valid_import_names + assert len(namespace.valid_import_names) == 3 # MathUtils, PI, and square + + # Check usages + assert {namespace.export}.issubset(namespace.symbol_usages(UsageType.DIRECT)) + assert {calc_area}.issubset(namespace.symbol_usages) + + # PI has direct usage (export) and chained usage (in calculateArea) + assert set(pi.symbol_usages(UsageType.DIRECT)) == {pi.export} + # assert set(pi.symbol_usages(UsageType.CHAINED)) == {calc_area} + assert set(pi.symbol_usages) == {pi.export, calc_area} + + # square has direct usage (export) and chained usage (in calculateArea) + assert set(square.symbol_usages(UsageType.DIRECT)) == {square.export} + assert set(square.symbol_usages(UsageType.CHAINED)) == {calc_area} + assert set(square.symbol_usages) == {square.export, calc_area} + + # Verify attribute resolution + assert namespace.resolve_attribute("PI") == pi + assert namespace.resolve_attribute("square") == square + + +def test_namespace_cross_file_usage(tmpdir) -> None: + """Test namespace usage across files with imports.""" + # language=typescript + content1 = """ + export namespace MathUtils { + export const PI = 3.14159; + export function square(x: number) { return x * x; } + const internal = 123; // not exported + } + """ + # language=typescript + content2 = """ + import { MathUtils } from './file1'; + + function calculateArea(radius: number) { + return MathUtils.PI * MathUtils.square(radius); + } + + function calculateVolume(radius: number) { + const area = calculateArea(radius); + return area * radius; + } + """ + with get_codebase_session(tmpdir=tmpdir, files={"file1.ts": content1, "file2.ts": content2}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase: + file1 = codebase.get_file("file1.ts") + file2 = codebase.get_file("file2.ts") + + # Get symbols + namespace = file1.get_symbol("MathUtils") + pi = namespace.get_symbol("PI") + square = namespace.get_symbol("square") + internal = namespace.get_symbol("internal") + calc_area = file2.get_function("calculateArea") + calc_volume = file2.get_function("calculateVolume") + namespace_import = file2.get_import("MathUtils") + + # Check namespace visibility + assert "MathUtils" in namespace.valid_import_names + assert "PI" in namespace.valid_import_names + assert "square" in namespace.valid_import_names + assert "internal" not in namespace.valid_import_names + assert internal is None # private symbol not accessible + + # Check direct vs chained usages + assert {namespace.export}.issubset(namespace.symbol_usages(UsageType.DIRECT)) + assert {namespace.export, calc_area}.issubset(namespace.symbol_usages) + assert {pi.export}.issubset(pi.symbol_usages(UsageType.DIRECT)) + # assert {pi.export, calc_area}.issubset(pi.symbol_usages) + # assert {calc_area}.issubset(square.symbol_usages(UsageType.CHAINED)) + + # Verify attribute resolution + assert namespace.resolve_attribute("PI") == pi + assert namespace.resolve_attribute("square") == square + assert namespace.resolve_attribute("internal") is None From 6cf569b2f648a49eac896cc72028ea84fa2b42f0 Mon Sep 17 00:00:00 2001 From: tomcodgen <191515280+tomcodgen@users.noreply.github.com> Date: Fri, 7 Feb 2025 03:44:46 +0000 Subject: [PATCH 02/13] Automated pre-commit update --- ruff.toml | 4 ++++ src/codegen/sdk/typescript/namespace.py | 4 ++-- src/codegen/shared/compilation/function_imports.py | 4 ++++ .../codegen/sdk/typescript/namespace/test_namespace_usage.py | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ruff.toml b/ruff.toml index f30d38974..42926d6ed 100644 --- a/ruff.toml +++ b/ruff.toml @@ -210,6 +210,10 @@ extend-generics = [ "codegen.sdk.typescript.import_resolution.TSImport", "codegen.sdk.typescript.interface.TSInterface", "codegen.sdk.typescript.interfaces.has_block.TSHasBlock", + "codegen.sdk.typescript.namespace.add_symbol", + "codegen.sdk.typescript.namespace.remove_symbol", + "codegen.sdk.typescript.namespace.rename_symbol", + "codegen.sdk.typescript.namespace.resolve_attribute", "codegen.sdk.typescript.namespace.TSNamespace", "codegen.sdk.typescript.placeholder.placeholder_return_type.TSReturnTypePlaceholder", "codegen.sdk.typescript.statements.assignment_statement.TSAssignmentStatement", diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index e946d49d2..86c40ebe3 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -4,7 +4,6 @@ from codegen.sdk.core.autocommit import commiter from codegen.sdk.core.interfaces.has_name import HasName -from codegen.sdk.core.statements.symbol_statement import SymbolStatement from codegen.sdk.enums import SymbolType from codegen.sdk.extensions.utils import cached_property from codegen.sdk.typescript.class_definition import TSClass @@ -279,7 +278,8 @@ def add_symbol(self, symbol: TSSymbol | str, export: bool = False, remove_origin self.G.commit_transactions() added_symbol = self.get_symbol(symbol_name) if added_symbol is None: - raise ValueError(f"Failed to add symbol {symbol_name} to namespace") + msg = f"Failed to add symbol {symbol_name} to namespace" + raise ValueError(msg) return added_symbol @ts_apidoc diff --git a/src/codegen/shared/compilation/function_imports.py b/src/codegen/shared/compilation/function_imports.py index c020230b5..d3177e571 100644 --- a/src/codegen/shared/compilation/function_imports.py +++ b/src/codegen/shared/compilation/function_imports.py @@ -175,6 +175,10 @@ def get_generated_imports(): from codegen.sdk.typescript.interface import TSInterface from codegen.sdk.typescript.interfaces.has_block import TSHasBlock from codegen.sdk.typescript.namespace import TSNamespace +from codegen.sdk.typescript.namespace import add_symbol +from codegen.sdk.typescript.namespace import remove_symbol +from codegen.sdk.typescript.namespace import rename_symbol +from codegen.sdk.typescript.namespace import resolve_attribute from codegen.sdk.typescript.placeholder.placeholder_return_type import TSReturnTypePlaceholder from codegen.sdk.typescript.statements.assignment_statement import TSAssignmentStatement from codegen.sdk.typescript.statements.attribute import TSAttribute diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py index fa6567a70..268ab7112 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py @@ -1,6 +1,6 @@ from codegen.sdk.codebase.factory.get_session import get_codebase_session -from codegen.sdk.enums import ProgrammingLanguage from codegen.sdk.core.dataclasses.usage import UsageType +from codegen.sdk.enums import ProgrammingLanguage def test_namespace_same_file_usage(tmpdir) -> None: From 1c6e694cde3b761469712fc39a04bd9c9a728620 Mon Sep 17 00:00:00 2001 From: tomcodegen Date: Thu, 6 Feb 2025 20:33:50 -0800 Subject: [PATCH 03/13] update --- .../sdk/typescript/import_resolution.py | 43 +++++++++++++-- src/codegen/sdk/typescript/namespace.py | 10 ++-- .../typescript/namespace/test_namespace.py | 43 +++++++++++++++ .../namespace/test_namespace_modifications.py | 52 +++++++++++++++++-- .../namespace/test_namespace_usage.py | 6 +-- 5 files changed, 139 insertions(+), 15 deletions(-) diff --git a/src/codegen/sdk/typescript/import_resolution.py b/src/codegen/sdk/typescript/import_resolution.py index de737bf16..fe4e79267 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -5,10 +5,12 @@ from typing import TYPE_CHECKING, Self, override from codegen.sdk.core.autocommit import reader +from codegen.sdk.core.dataclasses.usage import UsageKind from codegen.sdk.core.expressions import Name +from codegen.sdk.core.expressions.chained_attribute import ChainedAttribute from codegen.sdk.core.import_resolution import Import, ImportResolution, WildcardImport from codegen.sdk.core.interfaces.exportable import Exportable -from codegen.sdk.enums import ImportType, NodeType +from codegen.sdk.enums import ImportType, NodeType, SymbolType from codegen.sdk.utils import find_all_descendants, find_first_ancestor, find_first_descendant from codegen.shared.decorators.docs import noapidoc, ts_apidoc @@ -153,6 +155,27 @@ def resolved_symbol(self) -> Symbol | ExternalModule | TSFile | None: if resolved_symbol is None: return None + # Track namespace-chained usages before any resolution happens + if hasattr(resolved_symbol, 'get_symbol'): + for usage in self.usages: + if isinstance(usage.match, ChainedAttribute): + # Get the accessed symbol through namespace + if accessed := resolved_symbol.get_symbol(usage.match.attribute.source): + # Create bi-directional usage relationship + accessed.add_usage( + usage.usage_symbol, + UsageKind.CHAINED, + usage.match, + self.G + ) + # The namespace itself maintains direct usage + resolved_symbol.add_usage( + usage.usage_symbol, + UsageKind.DIRECT, + usage.match, + self.G + ) + # If the default import is a single symbol export, resolve to the symbol if self.is_default_import(): if resolved_symbol is not None and resolved_symbol.node_type == NodeType.FILE: @@ -590,14 +613,26 @@ def namespace_imports(self) -> list[TSNamespace]: return [resolved] + @property def is_namespace_import(self) -> bool: """Returns True if this import is importing a namespace. Examples: - import * as MyNS from './mymodule'; # True - import { foo } from './mymodule'; # False + import { MathUtils } from './file1'; # True if MathUtils is a namespace + import * as AllUtils from './utils'; # True """ - return self.import_type == ImportType.NAMESPACE and self.alias is not None + # For wildcard imports with namespace alias + if self.import_type == ImportType.WILDCARD and self.namespace: + return True + + # For named imports, check if any imported symbol is a namespace + if self.import_type == ImportType.NAMED_EXPORT: + for name, _ in self.names: + symbol = self.resolved_symbol + if symbol and symbol.symbol_type == SymbolType.Namespace: + return True + + return False @override def set_import_module(self, new_module: str) -> None: diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index 86c40ebe3..697cd675e 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -3,8 +3,9 @@ from typing import TYPE_CHECKING from codegen.sdk.core.autocommit import commiter +from codegen.sdk.core.dataclasses.usage import Usage, UsageType from codegen.sdk.core.interfaces.has_name import HasName -from codegen.sdk.enums import SymbolType +from codegen.sdk.enums import EdgeType, SymbolType from codegen.sdk.extensions.utils import cached_property from codegen.sdk.typescript.class_definition import TSClass from codegen.sdk.typescript.enum_definition import TSEnum @@ -264,11 +265,12 @@ def add_symbol(self, symbol: TSSymbol | str, export: bool = False, remove_origin if isinstance(symbol, str): if export and not symbol.startswith("export "): symbol = f"export {symbol}" + self.code_block.statements.append(symbol) elif isinstance(symbol, TSSymbol): + source = symbol.source if export and not symbol.is_exported: - export_src = f"export {symbol.source};" - self.code_block.statements.append(export_src) - self.code_block.statements.append(symbol) + source = f"export {source}" + self.code_block.statements.append(source) self.G.commit_transactions() # Remove symbol from original location if remove_original is True diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py index b7ad2329a..cb600a439 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py @@ -332,3 +332,46 @@ def test_namespace_nested_deep(tmpdir) -> None: assert len(nested) == 2 # Should find B and C assert all(isinstance(ns, TSNamespace) for ns in nested) assert {ns.name for ns in nested} == {"B", "C"} + + +def test_namespace_imports(tmpdir) -> None: + """Test importing and using namespaces.""" + FILE_NAME_1 = "math.ts" + # language=typescript + FILE_CONTENT_1 = """ + export namespace Math { + export const PI = 3.14159; + export function square(x: number) { return x * x; } + + export namespace Advanced { + export function cube(x: number) { return x * x * x; } + } + } + """ + + FILE_NAME_2 = "app.ts" + # language=typescript + FILE_CONTENT_2 = """ + import { Math } from './math'; + + console.log(Math.PI); + console.log(Math.square(5)); + console.log(Math.Advanced.cube(3)); + """ + + with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={FILE_NAME_1: FILE_CONTENT_1, FILE_NAME_2: FILE_CONTENT_2}) as codebase: + math_ns = codebase.get_symbol("Math") + assert math_ns is not None + assert math_ns.name == "Math" + + # Test namespace import resolution + file2 = codebase.get_file(FILE_NAME_2) + math_import = file2.get_import("Math") + assert math_import is not None + assert math_import.is_namespace_import + + # Test nested namespace access + advanced = math_ns.get_namespace("Advanced") + assert advanced is not None + assert advanced.name == "Advanced" + assert advanced.get_function("cube") is not None diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py index e9004c393..983a9a23f 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py @@ -1,5 +1,7 @@ from typing import TYPE_CHECKING +import pytest + from codegen.sdk.codebase.factory.get_session import get_codebase_session from codegen.sdk.enums import ProgrammingLanguage @@ -35,7 +37,7 @@ def test_namespace_add_symbol(tmpdir) -> None: original_parent.remove_symbol(new_const.name) # Add to namespace by moving operation - # namespace.add_symbol(new_const, export=True, move=True) + namespace.add_symbol(new_const, export=True, move=True) codebase.G.commit_transactions() @@ -45,8 +47,8 @@ def test_namespace_add_symbol(tmpdir) -> None: # Verify symbols were moved correctly assert namespace.get_symbol("ya") is not None assert file.get_symbol("ya") is None # Should no longer exist in file directly - # assert namespace.get_symbol("yb") is not None - # assert file.get_symbol("yb") is None # Should no longer exist in file directly + assert namespace.get_symbol("yb") is not None + assert file.get_symbol("yb") is None # Should no longer exist in file directly # 2. Add new exported symbol from string exported_code = "const z = 3" @@ -58,7 +60,7 @@ def test_namespace_add_symbol(tmpdir) -> None: assert exported.parent.ts_node_type == "export_statement" assert len(namespace.symbols) == 2 - assert {s.name for s in namespace.symbols} == {"x", "z"} + assert {s.name for s in namespace.symbols} == {"x", "z", "ya", "yb"} def test_namespace_remove_symbol(tmpdir) -> None: @@ -139,3 +141,45 @@ def test_namespace_export_symbol(tmpdir) -> None: external = namespace.get_symbol("external") assert external is not None assert external.is_exported + + +@pytest.mark.skip("TODO: Symbol Animals is ambiguous in codebase - more than one instance") +def test_namespace_merging(tmpdir) -> None: + """Test TypeScript namespace merging functionality.""" + FILE_NAME = "test.ts" + # language=typescript + FILE_CONTENT = """ + namespace Animals { + export class Dog { bark() {} } + } + + namespace Animals { // Merge with previous namespace + export class Cat { meow() {} } + } + + namespace Plants { // Different namespace, should not merge + export class Tree {} + } + """ + with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={FILE_NAME: FILE_CONTENT}) as codebase: + animals = codebase.get_symbol("Animals") + assert animals is not None + + # Test merged namespace access + assert animals.get_class("Dog") is not None + assert animals.get_class("Cat") is not None + + # Verify merged namespaces + assert len(animals.merged_namespaces) == 1 + merged = animals.merged_namespaces[0] + assert merged.name == "Animals" + assert merged != animals + + # Verify all symbols accessible + all_symbols = animals.symbols + assert len(all_symbols) == 2 + assert {s.name for s in all_symbols} == {"Dog", "Cat"} + + # Verify non-merged namespace + plants = codebase.get_symbol("Plants") + assert len(plants.merged_namespaces) == 0 diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py index 268ab7112..d9e146062 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py @@ -35,7 +35,7 @@ def test_namespace_same_file_usage(tmpdir) -> None: # PI has direct usage (export) and chained usage (in calculateArea) assert set(pi.symbol_usages(UsageType.DIRECT)) == {pi.export} - # assert set(pi.symbol_usages(UsageType.CHAINED)) == {calc_area} + assert set(pi.symbol_usages(UsageType.CHAINED)) == {calc_area} assert set(pi.symbol_usages) == {pi.export, calc_area} # square has direct usage (export) and chained usage (in calculateArea) @@ -95,8 +95,8 @@ def test_namespace_cross_file_usage(tmpdir) -> None: assert {namespace.export}.issubset(namespace.symbol_usages(UsageType.DIRECT)) assert {namespace.export, calc_area}.issubset(namespace.symbol_usages) assert {pi.export}.issubset(pi.symbol_usages(UsageType.DIRECT)) - # assert {pi.export, calc_area}.issubset(pi.symbol_usages) - # assert {calc_area}.issubset(square.symbol_usages(UsageType.CHAINED)) + assert {pi.export, calc_area}.issubset(pi.symbol_usages) + assert {calc_area}.issubset(square.symbol_usages(UsageType.CHAINED)) # Verify attribute resolution assert namespace.resolve_attribute("PI") == pi From 7b1343376c9bc730b014a78a1c83e25abc07431d Mon Sep 17 00:00:00 2001 From: tomcodgen <191515280+tomcodgen@users.noreply.github.com> Date: Fri, 7 Feb 2025 04:43:21 +0000 Subject: [PATCH 04/13] Automated pre-commit update --- src/codegen/sdk/typescript/import_resolution.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/codegen/sdk/typescript/import_resolution.py b/src/codegen/sdk/typescript/import_resolution.py index fe4e79267..d36744cec 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -156,25 +156,15 @@ def resolved_symbol(self) -> Symbol | ExternalModule | TSFile | None: return None # Track namespace-chained usages before any resolution happens - if hasattr(resolved_symbol, 'get_symbol'): + if hasattr(resolved_symbol, "get_symbol"): for usage in self.usages: if isinstance(usage.match, ChainedAttribute): # Get the accessed symbol through namespace if accessed := resolved_symbol.get_symbol(usage.match.attribute.source): # Create bi-directional usage relationship - accessed.add_usage( - usage.usage_symbol, - UsageKind.CHAINED, - usage.match, - self.G - ) + accessed.add_usage(usage.usage_symbol, UsageKind.CHAINED, usage.match, self.G) # The namespace itself maintains direct usage - resolved_symbol.add_usage( - usage.usage_symbol, - UsageKind.DIRECT, - usage.match, - self.G - ) + resolved_symbol.add_usage(usage.usage_symbol, UsageKind.DIRECT, usage.match, self.G) # If the default import is a single symbol export, resolve to the symbol if self.is_default_import(): From bc810bd8f1f88ef2803e172649cd3ae38f35fe78 Mon Sep 17 00:00:00 2001 From: tkucar Date: Wed, 12 Mar 2025 19:25:55 +0100 Subject: [PATCH 05/13] ns --- ruff.toml | 4 - .../sdk/core/expressions/chained_attribute.py | 7 +- .../sdk/typescript/import_resolution.py | 4 +- src/codegen/sdk/typescript/namespace.py | 137 +++++++++++------- .../shared/compilation/function_imports.py | 4 - .../namespace/test_namespace_modifications.py | 42 +++--- .../namespace/test_namespace_usage.py | 1 - 7 files changed, 110 insertions(+), 89 deletions(-) diff --git a/ruff.toml b/ruff.toml index 2786791fe..301031822 100644 --- a/ruff.toml +++ b/ruff.toml @@ -213,10 +213,6 @@ extend-generics = [ "codegen.sdk.typescript.import_resolution.TSImport", "codegen.sdk.typescript.interface.TSInterface", "codegen.sdk.typescript.interfaces.has_block.TSHasBlock", - "codegen.sdk.typescript.namespace.add_symbol", - "codegen.sdk.typescript.namespace.remove_symbol", - "codegen.sdk.typescript.namespace.rename_symbol", - "codegen.sdk.typescript.namespace.resolve_attribute", "codegen.sdk.typescript.namespace.TSNamespace", "codegen.sdk.typescript.placeholder.placeholder_return_type.TSReturnTypePlaceholder", "codegen.sdk.typescript.statements.assignment_statement.TSAssignmentStatement", diff --git a/src/codegen/sdk/core/expressions/chained_attribute.py b/src/codegen/sdk/core/expressions/chained_attribute.py index 04704fbbc..d29304291 100644 --- a/src/codegen/sdk/core/expressions/chained_attribute.py +++ b/src/codegen/sdk/core/expressions/chained_attribute.py @@ -142,11 +142,12 @@ def _resolved_types(self) -> Generator[ResolutionStack[Self], None, None]: # Module imports yield from self.with_resolution_frame(res) return - # HACK: This is a hack to skip the resolved types for namespaces - if isinstance(self.object, TSNamespace): - return + # # HACK: This is a hack to skip the resolved types for namespaces + # if isinstance(self.object, TSNamespace): + # return for resolved_type in self.object.resolved_type_frames: top = resolved_type.top + if not isinstance(top.node, HasAttribute): generics: dict = resolved_type.generics.copy() if top.node.source.lower() == "dict" and self.attribute.source in ("values", "get", "pop"): diff --git a/src/codegen/sdk/typescript/import_resolution.py b/src/codegen/sdk/typescript/import_resolution.py index ec271e1a9..7fc3c00ce 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -162,9 +162,9 @@ def resolved_symbol(self) -> Symbol | ExternalModule | TSFile | None: # Get the accessed symbol through namespace if accessed := resolved_symbol.get_symbol(usage.match.attribute.source): # Create bi-directional usage relationship - accessed.add_usage(usage.usage_symbol, UsageKind.CHAINED, usage.match, self.G) + accessed.add_usage(usage.usage_symbol, UsageKind.CHAINED, usage.match, self.ctx) # The namespace itself maintains direct usage - resolved_symbol.add_usage(usage.usage_symbol, UsageKind.DIRECT, usage.match, self.G) + resolved_symbol.add_usage(usage.usage_symbol, UsageKind.DIRECT, usage.match, self.ctx) # If the default import is a single symbol export, resolve to the symbol if self.is_default_import(): diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index 932290c9a..aa7da6c61 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -1,11 +1,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Sequence +from typing import TYPE_CHECKING, override from codegen.sdk.core.autocommit import commiter +from codegen.sdk.core.autocommit.decorators import writer from codegen.sdk.core.dataclasses.usage import Usage, UsageType +from codegen.sdk.core.export import Export +from codegen.sdk.core.interfaces.has_attribute import HasAttribute from codegen.sdk.core.interfaces.has_name import HasName +from codegen.sdk.core.interfaces.importable import Importable from codegen.sdk.enums import EdgeType, SymbolType +from codegen.sdk.extensions.sort import sort_editables +from codegen.sdk.extensions.autocommit import reader from codegen.sdk.extensions.utils import cached_property from codegen.sdk.typescript.class_definition import TSClass from codegen.sdk.typescript.enum_definition import TSEnum @@ -25,10 +32,11 @@ from codegen.sdk.core.statements.statement import Statement from codegen.sdk.core.symbol import Symbol from codegen.sdk.typescript.detached_symbols.code_block import TSCodeBlock + from codegen.sdk.typescript.export import TSExport @ts_apidoc -class TSNamespace(TSSymbol, TSHasBlock, HasName): +class TSNamespace(TSSymbol, TSHasBlock, HasName, HasAttribute): """Representation of a namespace module in TypeScript. Attributes: @@ -55,19 +63,6 @@ def _compute_dependencies(self, usage_type: UsageKind | None = None, dest: HasNa """ # Use self as destination if none provided dest = dest or self.self_dest - - if dest and dest != self: - # Add direct usage of namespace itself - usage = Usage(kind=usage_type, match=self, usage_type=UsageType.DIRECT, usage_symbol=dest.parent_symbol, imported_by=None) - self.G.add_edge(self.node_id, dest.node_id, EdgeType.SYMBOL_USAGE, usage) - - # For each exported symbol accessed through namespace - for symbol in self.symbols: - if symbol and symbol.ts_node_type == "export_statement": - # Add chained usage edge - chained_usage = Usage(kind=usage_type, match=symbol, usage_type=UsageType.CHAINED, usage_symbol=dest.parent_symbol, imported_by=None) - self.G.add_edge(symbol.node_id, dest.node_id, EdgeType.SYMBOL_USAGE, chained_usage) - # Compute dependencies from namespace's code block self.code_block._compute_dependencies(usage_type, dest) @@ -121,6 +116,35 @@ def get_symbol(self, name: str, recursive: bool = True, get_private: bool = Fals return nested_symbol return None + + @reader(cache=False) + @noapidoc + def get_nodes(self, *, sort_by_id: bool = False, sort: bool = True) -> Sequence[Importable]: + """Returns all nodes in the namespace, sorted by position in the namespace.""" + file_nodes = self.file.get_nodes(sort_by_id=sort_by_id, sort=sort) + start_limit = self.start_byte + end_limit = self.end_byte + namespace_nodes = [] + for file_node in file_nodes: + if file_node.start_byte>start_limit: + if file_node.end_byte list[TSExport]: + """Returns all Export symbols in the namespace. + + Retrieves a list of all top-level export declarations in the current TypeScript namespace. + + Returns: + list[TSExport]: A list of TSExport objects representing all top-level export declarations in the namespace. + """ + # Filter to only get exports that are direct children of the namespace's code block + return sort_editables(filter(lambda node: isinstance(node, Export), self.get_nodes(sort=False)), by_id=True) @cached_property def functions(self) -> list[TSFunction]: @@ -234,18 +258,37 @@ def get_nested_namespaces(self) -> list[TSNamespace]: nested.extend(symbol.get_nested_namespaces()) return nested - @ts_apidoc + + @writer + def add_symbol_from_source(self, source: str) -> None: + """Adds a symbol to a namespace from a string representation. + + This method adds a new symbol definition to the namespace by appending its source code string. The symbol will be added + after existing symbols if present, otherwise at the beginning of the namespace. + + Args: + source (str): String representation of the symbol to be added. This should be valid source code for + the file's programming language. + + Returns: + None: The symbol is added directly to the namespace's content. + """ + symbols = self.symbols + if len(symbols) > 0: + symbols[-1].insert_after("\n" + source, fix_indentation=True) + else: + self.insert_after("\n" + source) + @commiter - def add_symbol(self, symbol: TSSymbol | str, export: bool = False, remove_original: bool = False) -> TSSymbol: - """Adds a new symbol to the namespace. + def add_symbol(self, symbol: TSSymbol, should_export: bool = True) -> TSSymbol|None: + """Adds a new symbol to the namespace, optionally exporting it if applicable. If the symbol already exists in the namespace, returns the existing symbol. Args: symbol: The symbol to add to the namespace (either a TSSymbol instance or source code string) - export: Whether to export the symbol. Defaults to False. - remove_original: Whether to remove the original symbol. Defaults to False. + export: Whether to export the symbol. Defaults to True. Returns: - The added symbol + TSSymbol | None: The existing symbol if it already exists in the file or None if it was added. """ # TODO: Do we need to check if symbol can be added to the namespace? # if not self.symbol_can_be_added(symbol): @@ -253,38 +296,30 @@ def add_symbol(self, symbol: TSSymbol | str, export: bool = False, remove_origin # TODO: add symbol by moving # TODO: use self.export_symbol() to export the symbol if needed ? - print("SYMBOL to be added: ", symbol, "export: ", export) - symbol_name = symbol.name if isinstance(symbol, TSSymbol) else symbol.split(" ")[2 if export else 1] + # print("SYMBOL to be added: ", symbol, "export: ", should_export) + # symbol_name = symbol.name if isinstance(symbol, TSSymbol) else symbol.split(" ")[2 if should_export else 1] # Check if the symbol already exists in file - existing_symbol = self.get_symbol(symbol_name) + existing_symbol = self.get_symbol(symbol.name) if existing_symbol is not None: return existing_symbol - - # Export symbol if needed, then append to code block - if isinstance(symbol, str): - if export and not symbol.startswith("export "): - symbol = f"export {symbol}" - self.code_block.statements.append(symbol) - elif isinstance(symbol, TSSymbol): - source = symbol.source - if export and not symbol.is_exported: - source = f"export {source}" - self.code_block.statements.append(source) - self.G.commit_transactions() - - # Remove symbol from original location if remove_original is True - if remove_original and symbol.parent is not None: - symbol.parent.remove_symbol(symbol_name) - - self.G.commit_transactions() - added_symbol = self.get_symbol(symbol_name) - if added_symbol is None: - msg = f"Failed to add symbol {symbol_name} to namespace" + + if not self.file.symbol_can_be_added(symbol): + msg = f"Symbol {symbol.name} cannot be added to this file type." raise ValueError(msg) - return added_symbol + + + source = symbol.source + if isinstance(symbol, TSFunction) and symbol.is_arrow: + raw_source = symbol._named_arrow_function.text.decode("utf-8") + else: + raw_source = symbol.ts_node.text.decode("utf-8") + if should_export and hasattr(symbol, "export") and (not symbol.is_exported or raw_source not in symbol.export.source): + source = source.replace(source, f"export {source}") + self.add_symbol_from_source(source) + + - @ts_apidoc @commiter def remove_symbol(self, symbol_name: str) -> TSSymbol | None: """Removes a symbol from the namespace by name. @@ -302,11 +337,9 @@ def remove_symbol(self, symbol_name: str) -> TSSymbol | None: if symbol.source == stmt.source: print("stmt to be removed: ", stmt) self.code_block.statements.pop(i) - self.G.commit_transactions() return symbol return None - @ts_apidoc @commiter def rename_symbol(self, old_name: str, new_name: str) -> None: """Renames a symbol within the namespace. @@ -318,7 +351,6 @@ def rename_symbol(self, old_name: str, new_name: str) -> None: symbol = self.get_symbol(old_name) if symbol: symbol.rename(new_name) - self.G.commit_transactions() @commiter def export_symbol(self, name: str) -> None: @@ -327,13 +359,12 @@ def export_symbol(self, name: str) -> None: Args: name: Name of symbol to export """ - symbol = self.get_symbol(name) + symbol = self.get_symbol(name,get_private=True) if not symbol or symbol.is_exported: return export_source = f"export {symbol.source}" symbol.parent.edit(export_source) - self.G.commit_transactions() @property def valid_import_names(self) -> set[str]: @@ -371,7 +402,7 @@ def resolve_import(self, import_name: str) -> Symbol | None: return None - @ts_apidoc + @override def resolve_attribute(self, name: str) -> Symbol | None: """Resolves an attribute access on the namespace. diff --git a/src/codegen/shared/compilation/function_imports.py b/src/codegen/shared/compilation/function_imports.py index 2b24c9a03..f8539926c 100644 --- a/src/codegen/shared/compilation/function_imports.py +++ b/src/codegen/shared/compilation/function_imports.py @@ -176,10 +176,6 @@ def get_generated_imports(): from codegen.sdk.typescript.interface import TSInterface from codegen.sdk.typescript.interfaces.has_block import TSHasBlock from codegen.sdk.typescript.namespace import TSNamespace -from codegen.sdk.typescript.namespace import add_symbol -from codegen.sdk.typescript.namespace import remove_symbol -from codegen.sdk.typescript.namespace import rename_symbol -from codegen.sdk.typescript.namespace import resolve_attribute from codegen.sdk.typescript.placeholder.placeholder_return_type import TSReturnTypePlaceholder from codegen.sdk.typescript.statements.assignment_statement import TSAssignmentStatement from codegen.sdk.typescript.statements.attribute import TSAttribute diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py index 983a9a23f..cc86e764b 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py @@ -25,42 +25,36 @@ def test_namespace_add_symbol(tmpdir) -> None: # 1. a) Add new symbol from object, then manually remove the original symbol from the file # 1. b) Add new symbol by moving operation file.add_symbol_from_source(source="const ya = 2") - file.add_symbol_from_source(source="const yb = 2") - codebase.G.commit_transactions() + codebase.ctx.commit_transactions() new_const = file.get_symbol("ya") # Store original location - original_parent = new_const.parent # Add to namespace and remove from original location - namespace.add_symbol(new_const, export=True) - original_parent.remove_symbol(new_const.name) + namespace.add_symbol(new_const,should_export=True) - # Add to namespace by moving operation - namespace.add_symbol(new_const, export=True, move=True) - - codebase.G.commit_transactions() + codebase.ctx.commit_transactions() # Get fresh reference to namespace namespace: TSNamespace = codebase.get_symbol("MyNamespace") # Verify symbols were moved correctly assert namespace.get_symbol("ya") is not None - assert file.get_symbol("ya") is None # Should no longer exist in file directly - assert namespace.get_symbol("yb") is not None - assert file.get_symbol("yb") is None # Should no longer exist in file directly - - # 2. Add new exported symbol from string - exported_code = "const z = 3" - exported = namespace.add_symbol(exported_code, export=True) + assert namespace.get_symbol("ya").export is not None + + # 2. Add new symbol from string + code = "const z = 3" + namespace.add_symbol_from_source(code) + codebase.ctx.commit_transactions() + namespace: TSNamespace = codebase.get_symbol("MyNamespace") + code_symbol = namespace.get_symbol('z',get_private=True) # Verify exported symbol - assert exported is not None - assert exported.name == "z" - assert exported.parent.ts_node_type == "export_statement" + assert code_symbol is not None + assert code_symbol.name == "z" - assert len(namespace.symbols) == 2 - assert {s.name for s in namespace.symbols} == {"x", "z", "ya", "yb"} + assert len(namespace.symbols) == 3 + assert {s.name for s in namespace.symbols} == {"x", "ya", "z"} def test_namespace_remove_symbol(tmpdir) -> None: @@ -78,6 +72,7 @@ def test_namespace_remove_symbol(tmpdir) -> None: # Remove existing symbol removed = namespace.remove_symbol("x") + codebase.ctx.commit_transactions() assert removed is not None assert removed.name == "x" @@ -104,7 +99,7 @@ def test_namespace_rename(tmpdir) -> None: # Rename namespace namespace.rename("NewName") - codebase.G.commit_transactions() + codebase.ctx.commit_transactions() # Verify rename namespace: TSNamespace = codebase.get_symbol("NewName") @@ -128,6 +123,7 @@ def test_namespace_export_symbol(tmpdir) -> None: # Export internal symbol namespace.export_symbol("internal") + codebase.ctx.commit_transactions() # Verify export namespace: TSNamespace = codebase.get_symbol("ExportTest") @@ -137,6 +133,8 @@ def test_namespace_export_symbol(tmpdir) -> None: # Export already exported symbol (no change) namespace.export_symbol("external") + codebase.ctx.commit_transactions() + namespace: TSNamespace = codebase.get_symbol("ExportTest") external = namespace.get_symbol("external") assert external is not None diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py index d9e146062..867a4cfff 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py @@ -30,7 +30,6 @@ def test_namespace_same_file_usage(tmpdir) -> None: assert len(namespace.valid_import_names) == 3 # MathUtils, PI, and square # Check usages - assert {namespace.export}.issubset(namespace.symbol_usages(UsageType.DIRECT)) assert {calc_area}.issubset(namespace.symbol_usages) # PI has direct usage (export) and chained usage (in calculateArea) From 6fe9b1b6661e1260687f5a0bf317037d7ca9e75a Mon Sep 17 00:00:00 2001 From: tomcodgen <191515280+tomcodgen@users.noreply.github.com> Date: Wed, 12 Mar 2025 18:26:52 +0000 Subject: [PATCH 06/13] Automated pre-commit update --- .../sdk/core/expressions/chained_attribute.py | 2 -- src/codegen/sdk/typescript/namespace.py | 28 ++++++++----------- .../namespace/test_namespace_modifications.py | 6 ++-- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/codegen/sdk/core/expressions/chained_attribute.py b/src/codegen/sdk/core/expressions/chained_attribute.py index d29304291..26187fa3b 100644 --- a/src/codegen/sdk/core/expressions/chained_attribute.py +++ b/src/codegen/sdk/core/expressions/chained_attribute.py @@ -134,8 +134,6 @@ def object(self) -> Object: @noapidoc @override def _resolved_types(self) -> Generator[ResolutionStack[Self], None, None]: - from codegen.sdk.typescript.namespace import TSNamespace - if not self.ctx.config.method_usages: return if res := self.file.valid_import_names.get(self.full_name, None): diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index aa7da6c61..a41378737 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -1,18 +1,15 @@ from __future__ import annotations -from collections.abc import Sequence from typing import TYPE_CHECKING, override from codegen.sdk.core.autocommit import commiter from codegen.sdk.core.autocommit.decorators import writer -from codegen.sdk.core.dataclasses.usage import Usage, UsageType from codegen.sdk.core.export import Export from codegen.sdk.core.interfaces.has_attribute import HasAttribute from codegen.sdk.core.interfaces.has_name import HasName -from codegen.sdk.core.interfaces.importable import Importable -from codegen.sdk.enums import EdgeType, SymbolType -from codegen.sdk.extensions.sort import sort_editables +from codegen.sdk.enums import SymbolType from codegen.sdk.extensions.autocommit import reader +from codegen.sdk.extensions.sort import sort_editables from codegen.sdk.extensions.utils import cached_property from codegen.sdk.typescript.class_definition import TSClass from codegen.sdk.typescript.enum_definition import TSEnum @@ -24,10 +21,13 @@ from codegen.shared.decorators.docs import noapidoc, ts_apidoc if TYPE_CHECKING: + from collections.abc import Sequence + from tree_sitter import Node as TSNode from codegen.sdk.codebase.codebase_context import CodebaseContext from codegen.sdk.core.dataclasses.usage import UsageKind + from codegen.sdk.core.interfaces.importable import Importable from codegen.sdk.core.node_id_factory import NodeId from codegen.sdk.core.statements.statement import Statement from codegen.sdk.core.symbol import Symbol @@ -116,7 +116,7 @@ def get_symbol(self, name: str, recursive: bool = True, get_private: bool = Fals return nested_symbol return None - + @reader(cache=False) @noapidoc def get_nodes(self, *, sort_by_id: bool = False, sort: bool = True) -> Sequence[Importable]: @@ -126,13 +126,13 @@ def get_nodes(self, *, sort_by_id: bool = False, sort: bool = True) -> Sequence[ end_limit = self.end_byte namespace_nodes = [] for file_node in file_nodes: - if file_node.start_byte>start_limit: - if file_node.end_byte start_limit: + if file_node.end_byte < end_limit: namespace_nodes.append(file_node) else: break return namespace_nodes - + @cached_property @reader(cache=False) def exports(self) -> list[TSExport]: @@ -258,7 +258,6 @@ def get_nested_namespaces(self) -> list[TSNamespace]: nested.extend(symbol.get_nested_namespaces()) return nested - @writer def add_symbol_from_source(self, source: str) -> None: """Adds a symbol to a namespace from a string representation. @@ -280,7 +279,7 @@ def add_symbol_from_source(self, source: str) -> None: self.insert_after("\n" + source) @commiter - def add_symbol(self, symbol: TSSymbol, should_export: bool = True) -> TSSymbol|None: + def add_symbol(self, symbol: TSSymbol, should_export: bool = True) -> TSSymbol | None: """Adds a new symbol to the namespace, optionally exporting it if applicable. If the symbol already exists in the namespace, returns the existing symbol. Args: @@ -303,11 +302,10 @@ def add_symbol(self, symbol: TSSymbol, should_export: bool = True) -> TSSymbol|N existing_symbol = self.get_symbol(symbol.name) if existing_symbol is not None: return existing_symbol - + if not self.file.symbol_can_be_added(symbol): msg = f"Symbol {symbol.name} cannot be added to this file type." raise ValueError(msg) - source = symbol.source if isinstance(symbol, TSFunction) and symbol.is_arrow: @@ -318,8 +316,6 @@ def add_symbol(self, symbol: TSSymbol, should_export: bool = True) -> TSSymbol|N source = source.replace(source, f"export {source}") self.add_symbol_from_source(source) - - @commiter def remove_symbol(self, symbol_name: str) -> TSSymbol | None: """Removes a symbol from the namespace by name. @@ -359,7 +355,7 @@ def export_symbol(self, name: str) -> None: Args: name: Name of symbol to export """ - symbol = self.get_symbol(name,get_private=True) + symbol = self.get_symbol(name, get_private=True) if not symbol or symbol.is_exported: return diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py index cc86e764b..14ea765a7 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py @@ -31,7 +31,7 @@ def test_namespace_add_symbol(tmpdir) -> None: # Store original location # Add to namespace and remove from original location - namespace.add_symbol(new_const,should_export=True) + namespace.add_symbol(new_const, should_export=True) codebase.ctx.commit_transactions() @@ -41,14 +41,14 @@ def test_namespace_add_symbol(tmpdir) -> None: # Verify symbols were moved correctly assert namespace.get_symbol("ya") is not None assert namespace.get_symbol("ya").export is not None - + # 2. Add new symbol from string code = "const z = 3" namespace.add_symbol_from_source(code) codebase.ctx.commit_transactions() namespace: TSNamespace = codebase.get_symbol("MyNamespace") - code_symbol = namespace.get_symbol('z',get_private=True) + code_symbol = namespace.get_symbol("z", get_private=True) # Verify exported symbol assert code_symbol is not None assert code_symbol.name == "z" From 0a194aca938d1544ca19d7686e6348061b66c676 Mon Sep 17 00:00:00 2001 From: tkucar Date: Thu, 13 Mar 2025 01:04:51 +0100 Subject: [PATCH 07/13] rm --- src/codegen/sdk/core/expressions/chained_attribute.py | 4 +--- src/codegen/sdk/typescript/namespace.py | 10 ---------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/codegen/sdk/core/expressions/chained_attribute.py b/src/codegen/sdk/core/expressions/chained_attribute.py index 26187fa3b..ccd5a788f 100644 --- a/src/codegen/sdk/core/expressions/chained_attribute.py +++ b/src/codegen/sdk/core/expressions/chained_attribute.py @@ -140,9 +140,7 @@ def _resolved_types(self) -> Generator[ResolutionStack[Self], None, None]: # Module imports yield from self.with_resolution_frame(res) return - # # HACK: This is a hack to skip the resolved types for namespaces - # if isinstance(self.object, TSNamespace): - # return + for resolved_type in self.object.resolved_type_frames: top = resolved_type.top diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index a41378737..4dc46d4fb 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -289,16 +289,6 @@ def add_symbol(self, symbol: TSSymbol, should_export: bool = True) -> TSSymbol | Returns: TSSymbol | None: The existing symbol if it already exists in the file or None if it was added. """ - # TODO: Do we need to check if symbol can be added to the namespace? - # if not self.symbol_can_be_added(symbol): - # raise ValueError(f"Symbol {symbol.name} cannot be added to the namespace.") - # TODO: add symbol by moving - # TODO: use self.export_symbol() to export the symbol if needed ? - - # print("SYMBOL to be added: ", symbol, "export: ", should_export) - # symbol_name = symbol.name if isinstance(symbol, TSSymbol) else symbol.split(" ")[2 if should_export else 1] - - # Check if the symbol already exists in file existing_symbol = self.get_symbol(symbol.name) if existing_symbol is not None: return existing_symbol From c8825ddfc520bfb3eb09f3ce9851d683e184545e Mon Sep 17 00:00:00 2001 From: tomcodegen Date: Fri, 14 Mar 2025 21:15:05 -0700 Subject: [PATCH 08/13] ns fix --- .../sdk/typescript/import_resolution.py | 12 -------- .../test_import_resolution_resolve_import.py | 30 +++++++++++++++++++ .../namespace/test_namespace_modifications.py | 2 +- .../namespace/test_namespace_usage.py | 2 +- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/codegen/sdk/typescript/import_resolution.py b/src/codegen/sdk/typescript/import_resolution.py index 7fc3c00ce..0bec7735b 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -5,9 +5,7 @@ from typing import TYPE_CHECKING, Self, override from codegen.sdk.core.autocommit import reader -from codegen.sdk.core.dataclasses.usage import UsageKind from codegen.sdk.core.expressions import Name -from codegen.sdk.core.expressions.chained_attribute import ChainedAttribute from codegen.sdk.core.import_resolution import Import, ImportResolution, WildcardImport from codegen.sdk.core.interfaces.exportable import Exportable from codegen.sdk.enums import ImportType, NodeType, SymbolType @@ -155,16 +153,6 @@ def resolved_symbol(self) -> Symbol | ExternalModule | TSFile | None: if resolved_symbol is None: return None - # Track namespace-chained usages before any resolution happens - if hasattr(resolved_symbol, "get_symbol"): - for usage in self.usages: - if isinstance(usage.match, ChainedAttribute): - # Get the accessed symbol through namespace - if accessed := resolved_symbol.get_symbol(usage.match.attribute.source): - # Create bi-directional usage relationship - accessed.add_usage(usage.usage_symbol, UsageKind.CHAINED, usage.match, self.ctx) - # The namespace itself maintains direct usage - resolved_symbol.add_usage(usage.usage_symbol, UsageKind.DIRECT, usage.match, self.ctx) # If the default import is a single symbol export, resolve to the symbol if self.is_default_import(): diff --git a/tests/unit/codegen/sdk/typescript/import_resolution/test_import_resolution_resolve_import.py b/tests/unit/codegen/sdk/typescript/import_resolution/test_import_resolution_resolve_import.py index 5cbfcc7f6..49f0e13aa 100644 --- a/tests/unit/codegen/sdk/typescript/import_resolution/test_import_resolution_resolve_import.py +++ b/tests/unit/codegen/sdk/typescript/import_resolution/test_import_resolution_resolve_import.py @@ -834,3 +834,33 @@ def test_resolve_double_dynamic_import(tmpdir) -> None: assert len(bar.call_sites) == 1 assert foo.call_sites[0].source == "myFile2.foo()" assert bar.call_sites[0].source == "myFile3.bar()" + + + +def test_resolve_namespace_import(tmpdir) -> None: + # language=typescript + content = """ +import { CONSTS } from './file2' + +let use_a = CONSTS.a +let use_b = CONSTS.b +let use_c = CONSTS.c + + """ + # language=typescript + content2 = """ +export namespace CONSTS { + export const a = 2; + export const b = 3; + export const c = 4; +} + """ + with get_codebase_session(tmpdir=tmpdir, files={"file.ts": content, "file2.ts": content2}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase: + file = codebase.get_file("file.ts") + file2 = codebase.get_file("file2.ts") + assert len(file.imports) == 1 + + consts = file2.get_namespace("CONSTS") + + assert file.imports[0].resolved_symbol==consts + assert file.get_symbol("use_a").resolved_value==consts.get_symbol("a").resolved_value diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py index 14ea765a7..fc592beac 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py @@ -3,7 +3,7 @@ import pytest from codegen.sdk.codebase.factory.get_session import get_codebase_session -from codegen.sdk.enums import ProgrammingLanguage +from codegen.shared.enums.programming_language import ProgrammingLanguage if TYPE_CHECKING: from codegen.sdk.typescript.namespace import TSNamespace diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py index 867a4cfff..744ac51db 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py @@ -1,6 +1,6 @@ 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 +from codegen.shared.enums.programming_language import ProgrammingLanguage def test_namespace_same_file_usage(tmpdir) -> None: From f8c56b2a64c8dfd87ca7107a4c109df07d0223bd Mon Sep 17 00:00:00 2001 From: tomcodgen <191515280+tomcodgen@users.noreply.github.com> Date: Sat, 15 Mar 2025 04:15:54 +0000 Subject: [PATCH 09/13] Automated pre-commit update --- src/codegen/sdk/typescript/import_resolution.py | 1 - .../test_import_resolution_resolve_import.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/codegen/sdk/typescript/import_resolution.py b/src/codegen/sdk/typescript/import_resolution.py index 0bec7735b..07d71fa6a 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -153,7 +153,6 @@ def resolved_symbol(self) -> Symbol | ExternalModule | TSFile | None: if resolved_symbol is None: return None - # If the default import is a single symbol export, resolve to the symbol if self.is_default_import(): if resolved_symbol is not None and resolved_symbol.node_type == NodeType.FILE: diff --git a/tests/unit/codegen/sdk/typescript/import_resolution/test_import_resolution_resolve_import.py b/tests/unit/codegen/sdk/typescript/import_resolution/test_import_resolution_resolve_import.py index 49f0e13aa..e1ee905ab 100644 --- a/tests/unit/codegen/sdk/typescript/import_resolution/test_import_resolution_resolve_import.py +++ b/tests/unit/codegen/sdk/typescript/import_resolution/test_import_resolution_resolve_import.py @@ -836,7 +836,6 @@ def test_resolve_double_dynamic_import(tmpdir) -> None: assert bar.call_sites[0].source == "myFile3.bar()" - def test_resolve_namespace_import(tmpdir) -> None: # language=typescript content = """ @@ -862,5 +861,5 @@ def test_resolve_namespace_import(tmpdir) -> None: consts = file2.get_namespace("CONSTS") - assert file.imports[0].resolved_symbol==consts - assert file.get_symbol("use_a").resolved_value==consts.get_symbol("a").resolved_value + assert file.imports[0].resolved_symbol == consts + assert file.get_symbol("use_a").resolved_value == consts.get_symbol("a").resolved_value From 457c9ac7af93f221ed5c122822f358799017da85 Mon Sep 17 00:00:00 2001 From: tomcodegen Date: Tue, 18 Mar 2025 09:25:28 -0700 Subject: [PATCH 10/13] fix --- src/codegen/sdk/typescript/import_resolution.py | 1 + src/codegen/sdk/typescript/namespace.py | 17 +++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/codegen/sdk/typescript/import_resolution.py b/src/codegen/sdk/typescript/import_resolution.py index 07d71fa6a..a430c905c 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -9,6 +9,7 @@ from codegen.sdk.core.import_resolution import Import, ImportResolution, WildcardImport from codegen.sdk.core.interfaces.exportable import Exportable from codegen.sdk.enums import ImportType, NodeType, SymbolType +from codegen.sdk.typescript.namespace import TSNamespace from codegen.sdk.utils import find_all_descendants, find_first_ancestor, find_first_descendant from codegen.shared.decorators.docs import noapidoc, ts_apidoc diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index 4dc46d4fb..f3f399d28 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -155,21 +155,15 @@ def functions(self) -> list[TSFunction]: """ return [symbol for symbol in self.symbols if isinstance(symbol, TSFunction)] - def get_function(self, name: str, recursive: bool = True, use_full_name: bool = False) -> TSFunction | None: + def get_function(self, name: str, recursive: bool = True) -> TSFunction | None: """Get a function by name from this namespace. Args: - name: Name of the function to find (can be fully qualified like 'Outer.Inner.func') + name: Name of the function to find recursive: If True, also search in nested namespaces - use_full_name: If True, match against the full qualified name - - Returns: - TSFunction | None: The found function, or None if not found """ - if use_full_name and "." in name: - namespace_path, func_name = name.rsplit(".", 1) - target_ns = self.get_namespace(namespace_path) - return target_ns.get_function(func_name, recursive=False) if target_ns else None + # Import here to avoid circular import + from codegen.sdk.typescript.function import TSFunction symbol = self.get_symbol(name, recursive=recursive) return symbol if isinstance(symbol, TSFunction) else None @@ -190,6 +184,9 @@ def get_class(self, name: str, recursive: bool = True) -> TSClass | None: name: Name of the class to find recursive: If True, also search in nested namespaces """ + # Import here to avoid circular import + from codegen.sdk.typescript.class_definition import TSClass + symbol = self.get_symbol(name, recursive=recursive) return symbol if isinstance(symbol, TSClass) else None From b90d111d208851413854352de3fdb50a7af6357a Mon Sep 17 00:00:00 2001 From: tomcodegen Date: Tue, 18 Mar 2025 10:03:55 -0700 Subject: [PATCH 11/13] ns fix --- src/codegen/sdk/python/import_resolution.py | 2 +- .../sdk/typescript/import_resolution.py | 3 ++ src/codegen/sdk/typescript/namespace.py | 36 +++++++++---------- .../namespace/test_namespace_usage.py | 8 ++--- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/codegen/sdk/python/import_resolution.py b/src/codegen/sdk/python/import_resolution.py index 5c2a1f640..bf8e1cf49 100644 --- a/src/codegen/sdk/python/import_resolution.py +++ b/src/codegen/sdk/python/import_resolution.py @@ -15,12 +15,12 @@ from tree_sitter import Node as TSNode from codegen.sdk.codebase.codebase_context import CodebaseContext + from codegen.sdk.core.file import SourceFile from codegen.sdk.core.interfaces.editable import Editable from codegen.sdk.core.interfaces.exportable import Exportable from codegen.sdk.core.node_id_factory import NodeId from codegen.sdk.core.statements.import_statement import ImportStatement from codegen.sdk.python.file import PyFile - from src.codegen.sdk.core.file import SourceFile logger = get_logger(__name__) diff --git a/src/codegen/sdk/typescript/import_resolution.py b/src/codegen/sdk/typescript/import_resolution.py index a430c905c..2f74c31a1 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -25,6 +25,7 @@ from codegen.sdk.core.statements.import_statement import ImportStatement from codegen.sdk.core.symbol import Symbol from codegen.sdk.typescript.file import TSFile + from codegen.sdk.typescript.namespace import TSNamespace from codegen.sdk.typescript.statements.import_statement import TSImportStatement @@ -592,6 +593,8 @@ def namespace_imports(self) -> list[TSNamespace]: if not self.is_namespace_import(): return [] + from codegen.sdk.typescript.namespace import TSNamespace + resolved = self.resolved_symbol if resolved is None or not isinstance(resolved, TSNamespace): return [] diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index f3f399d28..2317e4e64 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -19,6 +19,7 @@ from codegen.sdk.typescript.symbol import TSSymbol from codegen.sdk.typescript.type_alias import TSTypeAlias from codegen.shared.decorators.docs import noapidoc, ts_apidoc +from codegen.shared.logging.get_logger import get_logger if TYPE_CHECKING: from collections.abc import Sequence @@ -33,8 +34,11 @@ from codegen.sdk.core.symbol import Symbol from codegen.sdk.typescript.detached_symbols.code_block import TSCodeBlock from codegen.sdk.typescript.export import TSExport + from codegen.sdk.typescript.import_resolution import TSImport +logger = get_logger(__name__) + @ts_apidoc class TSNamespace(TSSymbol, TSHasBlock, HasName, HasAttribute): """Representation of a namespace module in TypeScript. @@ -318,7 +322,7 @@ def remove_symbol(self, symbol_name: str) -> TSSymbol | None: # Remove from code block statements for i, stmt in enumerate(self.code_block.statements): if symbol.source == stmt.source: - print("stmt to be removed: ", stmt) + logger.debug(f"stmt to be removed: {stmt}") self.code_block.statements.pop(i) return symbol return None @@ -336,6 +340,7 @@ def rename_symbol(self, old_name: str, new_name: str) -> None: symbol.rename(new_name) @commiter + @noapidoc def export_symbol(self, name: str) -> None: """Marks a symbol as exported in the namespace. @@ -349,19 +354,22 @@ def export_symbol(self, name: str) -> None: export_source = f"export {symbol.source}" symbol.parent.edit(export_source) - @property - def valid_import_names(self) -> set[str]: + @cached_property + @noapidoc + @reader(cache=True) + def valid_import_names(self) -> dict[str, TSSymbol | TSImport]: """Returns set of valid import names for this namespace. This includes all exported symbols plus the namespace name itself for namespace imports. """ - names = {self.name} # Namespace itself can be imported - for stmt in self.code_block.statements: - if stmt.ts_node_type == "export_statement": - for export in stmt.exports: - names.add(export.name) - return names + valid_export_names = {} + valid_export_names[self.name]=self + for export in self.exports: + for name, dest in export.names: + valid_export_names[name] = dest + return valid_export_names + def resolve_import(self, import_name: str) -> Symbol | None: """Resolves an import name to a symbol within this namespace. @@ -395,13 +403,5 @@ def resolve_attribute(self, name: str) -> Symbol | None: Returns: The resolved symbol or None if not found """ - # First check direct symbols - if symbol := self.get_symbol(name): - return symbol - - # Then check nested namespaces recursively - for nested in self.get_nested_namespaces(): - if symbol := nested.get_symbol(name): - return symbol + return self.valid_import_names.get(name, None) - return None diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py index 744ac51db..9f72250b0 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py @@ -43,8 +43,8 @@ def test_namespace_same_file_usage(tmpdir) -> None: assert set(square.symbol_usages) == {square.export, calc_area} # Verify attribute resolution - assert namespace.resolve_attribute("PI") == pi - assert namespace.resolve_attribute("square") == square + assert namespace.resolve_attribute("PI") == pi.export + assert namespace.resolve_attribute("square") == square.export def test_namespace_cross_file_usage(tmpdir) -> None: @@ -98,6 +98,6 @@ def test_namespace_cross_file_usage(tmpdir) -> None: assert {calc_area}.issubset(square.symbol_usages(UsageType.CHAINED)) # Verify attribute resolution - assert namespace.resolve_attribute("PI") == pi - assert namespace.resolve_attribute("square") == square + assert namespace.resolve_attribute("PI") == pi.export + assert namespace.resolve_attribute("square") == square.export assert namespace.resolve_attribute("internal") is None From 2cd662c07400cdf88f50e631ce88c2b1ab0e4532 Mon Sep 17 00:00:00 2001 From: tomcodgen <191515280+tomcodgen@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:04:55 +0000 Subject: [PATCH 12/13] Automated pre-commit update --- src/codegen/sdk/typescript/namespace.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index 2317e4e64..994e2c8d7 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -39,6 +39,7 @@ logger = get_logger(__name__) + @ts_apidoc class TSNamespace(TSSymbol, TSHasBlock, HasName, HasAttribute): """Representation of a namespace module in TypeScript. @@ -364,13 +365,12 @@ def valid_import_names(self) -> dict[str, TSSymbol | TSImport]: for namespace imports. """ valid_export_names = {} - valid_export_names[self.name]=self + valid_export_names[self.name] = self for export in self.exports: for name, dest in export.names: valid_export_names[name] = dest return valid_export_names - def resolve_import(self, import_name: str) -> Symbol | None: """Resolves an import name to a symbol within this namespace. @@ -404,4 +404,3 @@ def resolve_attribute(self, name: str) -> Symbol | None: The resolved symbol or None if not found """ return self.valid_import_names.get(name, None) - From 9686d591c88d89fcdb5587fb9fd1508d3b836cc1 Mon Sep 17 00:00:00 2001 From: tomcodegen Date: Tue, 18 Mar 2025 11:02:45 -0700 Subject: [PATCH 13/13] fix --- .../sdk/typescript/import_resolution.py | 1 - src/codegen/sdk/typescript/namespace.py | 6 --- .../typescript/namespace/test_namespace.py | 38 ------------------- 3 files changed, 45 deletions(-) diff --git a/src/codegen/sdk/typescript/import_resolution.py b/src/codegen/sdk/typescript/import_resolution.py index 2f74c31a1..82b770a79 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -9,7 +9,6 @@ from codegen.sdk.core.import_resolution import Import, ImportResolution, WildcardImport from codegen.sdk.core.interfaces.exportable import Exportable from codegen.sdk.enums import ImportType, NodeType, SymbolType -from codegen.sdk.typescript.namespace import TSNamespace from codegen.sdk.utils import find_all_descendants, find_first_ancestor, find_first_descendant from codegen.shared.decorators.docs import noapidoc, ts_apidoc diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index 994e2c8d7..2442ce6da 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -167,9 +167,6 @@ def get_function(self, name: str, recursive: bool = True) -> TSFunction | None: name: Name of the function to find recursive: If True, also search in nested namespaces """ - # Import here to avoid circular import - from codegen.sdk.typescript.function import TSFunction - symbol = self.get_symbol(name, recursive=recursive) return symbol if isinstance(symbol, TSFunction) else None @@ -189,9 +186,6 @@ def get_class(self, name: str, recursive: bool = True) -> TSClass | None: name: Name of the class to find recursive: If True, also search in nested namespaces """ - # Import here to avoid circular import - from codegen.sdk.typescript.class_definition import TSClass - symbol = self.get_symbol(name, recursive=recursive) return symbol if isinstance(symbol, TSClass) else None diff --git a/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py index 09ccdff43..ab0764f76 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py @@ -123,44 +123,6 @@ def test_namespace_functions(tmpdir) -> None: assert all(func.is_exported for func in namespace.functions) -def test_namespace_function_full_name(tmpdir) -> None: - """Test getting functions using full names.""" - FILE_NAME = "test.ts" - # language=typescript - FILE_CONTENT = """ - namespace Outer { - export function shared() { return 1; } - export namespace Inner { - export function shared() { return 2; } - export function unique() { return 3; } - } - } - """ - with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={FILE_NAME: FILE_CONTENT}) as codebase: - namespace: TSNamespace = codebase.get_symbol("Outer") - assert namespace is not None - - # Test getting functions by local name - outer_shared = namespace.get_function("shared", recursive=False) - assert outer_shared is not None - inner_shared = namespace.get_function("shared", recursive=True) - assert inner_shared is not None - # Without full names, we might get either shared function - assert outer_shared == inner_shared - - # Test getting functions by full name - outer_shared = namespace.get_function("shared", use_full_name=True) - assert outer_shared is not None - inner_shared = namespace.get_function("Inner.shared", use_full_name=True) - assert inner_shared is not None - inner_unique = namespace.get_function("Inner.unique", use_full_name=True) - assert inner_unique is not None - - # Test non-existent paths - assert namespace.get_function("NonExistent.shared", use_full_name=True) is None - assert namespace.get_function("Inner.NonExistent", use_full_name=True) is None - - def test_namespace_function_overloading(tmpdir) -> None: """Test function overloading within namespace.""" FILE_NAME = "test.ts"