diff --git a/src/codegen/sdk/core/expressions/chained_attribute.py b/src/codegen/sdk/core/expressions/chained_attribute.py index 04704fbbc..ccd5a788f 100644 --- a/src/codegen/sdk/core/expressions/chained_attribute.py +++ b/src/codegen/sdk/core/expressions/chained_attribute.py @@ -134,19 +134,16 @@ 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): # 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 + 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/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 387ff2b14..82b770a79 100644 --- a/src/codegen/sdk/typescript/import_resolution.py +++ b/src/codegen/sdk/typescript/import_resolution.py @@ -8,7 +8,7 @@ from codegen.sdk.core.expressions import Name 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 @@ -24,6 +24,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 @@ -578,6 +579,48 @@ 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 [] + + from codegen.sdk.typescript.namespace import TSNamespace + + resolved = self.resolved_symbol + if resolved is None or not isinstance(resolved, TSNamespace): + return [] + + return [resolved] + + @property + def is_namespace_import(self) -> bool: + """Returns True if this import is importing a namespace. + + Examples: + import { MathUtils } from './file1'; # True if MathUtils is a namespace + import * as AllUtils from './utils'; # True + """ + # 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: """Sets the module of an import. diff --git a/src/codegen/sdk/typescript/namespace.py b/src/codegen/sdk/typescript/namespace.py index 4d1e3f7db..2442ce6da 100644 --- a/src/codegen/sdk/typescript/namespace.py +++ b/src/codegen/sdk/typescript/namespace.py @@ -1,11 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING +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.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.statements.symbol_statement import SymbolStatement 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 @@ -15,20 +19,29 @@ 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 + 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 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): +class TSNamespace(TSSymbol, TSHasBlock, HasName, HasAttribute): """Representation of a namespace module in TypeScript. Attributes: @@ -55,8 +68,7 @@ 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 + # Compute dependencies from namespace's code block self.code_block._compute_dependencies(usage_type, dest) @cached_property @@ -64,37 +76,81 @@ 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 + @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 < end_limit: + namespace_nodes.append(file_node) + else: + break + return namespace_nodes + + @cached_property + @reader(cache=False) + def exports(self) -> 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]: """Get all functions defined in this namespace. @@ -104,22 +160,13 @@ 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 - symbol = self.get_symbol(name, recursive=recursive) return symbol if isinstance(symbol, TSFunction) else None @@ -206,3 +253,148 @@ def get_nested_namespaces(self) -> list[TSNamespace]: nested.append(symbol) 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. + + 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, 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 True. + + Returns: + TSSymbol | None: The existing symbol if it already exists in the file or None if it was added. + """ + 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: + 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) + + @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: + logger.debug(f"stmt to be removed: {stmt}") + self.code_block.statements.pop(i) + return symbol + return None + + @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) + + @commiter + @noapidoc + 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, get_private=True) + if not symbol or symbol.is_exported: + return + + export_source = f"export {symbol.source}" + symbol.parent.edit(export_source) + + @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. + """ + 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. + + 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 + + @override + 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 + """ + return self.valid_import_names.get(name, None) 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..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 @@ -834,3 +834,32 @@ 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.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py index aed6271b9..ab0764f76 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace.py +++ b/tests/unit/codegen/sdk/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: @@ -124,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" @@ -333,3 +294,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_complex_examples.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_complex_examples.py index 3dfa77e28..9af4baf6f 100644 --- a/tests/unit/codegen/sdk/typescript/namespace/test_namespace_complex_examples.py +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_complex_examples.py @@ -133,3 +133,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/codegen/sdk/typescript/namespace/test_namespace_modifications.py b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py new file mode 100644 index 000000000..fc592beac --- /dev/null +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_modifications.py @@ -0,0 +1,183 @@ +from typing import TYPE_CHECKING + +import pytest + +from codegen.sdk.codebase.factory.get_session import get_codebase_session +from codegen.shared.enums.programming_language import ProgrammingLanguage + +if TYPE_CHECKING: + from codegen.sdk.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") + codebase.ctx.commit_transactions() + new_const = file.get_symbol("ya") + + # Store original location + + # Add to namespace and remove from original location + namespace.add_symbol(new_const, should_export=True) + + 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 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 code_symbol is not None + assert code_symbol.name == "z" + + assert len(namespace.symbols) == 3 + assert {s.name for s in namespace.symbols} == {"x", "ya", "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") + codebase.ctx.commit_transactions() + 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.ctx.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") + codebase.ctx.commit_transactions() + + # 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") + codebase.ctx.commit_transactions() + + namespace: TSNamespace = codebase.get_symbol("ExportTest") + 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 new file mode 100644 index 000000000..9f72250b0 --- /dev/null +++ b/tests/unit/codegen/sdk/typescript/namespace/test_namespace_usage.py @@ -0,0 +1,103 @@ +from codegen.sdk.codebase.factory.get_session import get_codebase_session +from codegen.sdk.core.dataclasses.usage import UsageType +from codegen.shared.enums.programming_language 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 {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.export + assert namespace.resolve_attribute("square") == square.export + + +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.export + assert namespace.resolve_attribute("square") == square.export + assert namespace.resolve_attribute("internal") is None