diff --git a/src/codegen/sdk/codebase/node_classes/node_classes.py b/src/codegen/sdk/codebase/node_classes/node_classes.py index 579063c3a..d2fa805ec 100644 --- a/src/codegen/sdk/codebase/node_classes/node_classes.py +++ b/src/codegen/sdk/codebase/node_classes/node_classes.py @@ -33,6 +33,7 @@ class NodeClasses: function_call_cls: type[FunctionCall] comment_cls: type[Comment] bool_conversion: dict[bool, str] + dynamic_import_parent_types: set[str] symbol_map: dict[str, type[Symbol]] = field(default_factory=dict) expression_map: dict[str, type[Expression]] = field(default_factory=dict) type_map: dict[str, type[Type] | dict[str, type[Type]]] = field(default_factory=dict) diff --git a/src/codegen/sdk/codebase/node_classes/py_node_classes.py b/src/codegen/sdk/codebase/node_classes/py_node_classes.py index 0bdb86b3e..533553e4e 100644 --- a/src/codegen/sdk/codebase/node_classes/py_node_classes.py +++ b/src/codegen/sdk/codebase/node_classes/py_node_classes.py @@ -109,4 +109,17 @@ def parse_subscript(node: TSNode, file_node_id, G, parent): True: "True", False: "False", }, + dynamic_import_parent_types={ + "function_definition", + "if_statement", + "try_statement", + "with_statement", + "else_clause", + "for_statement", + "except_clause", + "while_statement", + "match_statement", + "case_clause", + "finally_clause", + }, ) diff --git a/src/codegen/sdk/codebase/node_classes/ts_node_classes.py b/src/codegen/sdk/codebase/node_classes/ts_node_classes.py index 5ac4c3a8b..10610018f 100644 --- a/src/codegen/sdk/codebase/node_classes/ts_node_classes.py +++ b/src/codegen/sdk/codebase/node_classes/ts_node_classes.py @@ -163,4 +163,19 @@ def parse_new(node: TSNode, *args): True: "true", False: "false", }, + dynamic_import_parent_types={ + "function_declaration", + "method_definition", + "arrow_function", + "if_statement", + "try_statement", + "else_clause", + "catch_clause", + "finally_clause", + "while_statement", + "for_statement", + "do_statement", + "switch_case", + "switch_statement", + }, ) diff --git a/src/codegen/sdk/core/import_resolution.py b/src/codegen/sdk/core/import_resolution.py index e9d44e9c0..a4b9999b5 100644 --- a/src/codegen/sdk/core/import_resolution.py +++ b/src/codegen/sdk/core/import_resolution.py @@ -381,6 +381,54 @@ def imported_exports(self) -> list[Exportable]: For symbol imports, contains only the single imported symbol. """ + @property + @reader + def is_dynamic(self) -> bool: + """Determines if this import is dynamically loaded based on its parent symbol. + + A dynamic import is one that appears within control flow or scope-defining statements, such as: + - Inside function definitions + - Inside class definitions + - Inside if/else blocks + - Inside try/except blocks + - Inside with statements + + Dynamic imports are only loaded when their containing block is executed, unlike + top-level imports which are loaded when the module is imported. + + Examples: + Dynamic imports: + ```python + def my_function(): + import foo # Dynamic - only imported when function runs + + if condition: + from bar import baz # Dynamic - only imported if condition is True + + with context(): + import qux # Dynamic - only imported within context + ``` + + Static imports: + ```python + import foo # Static - imported when module loads + from bar import baz # Static - imported when module loads + ``` + + Returns: + bool: True if the import is dynamic (within a control flow or scope block), + False if it's a top-level import. + """ + curr = self.ts_node + + # always traverses upto the module level + while curr: + if curr.type in self.G.node_classes.dynamic_import_parent_types: + return True + curr = curr.parent + + return False + #################################################################################################################### # MANIPULATIONS #################################################################################################################### diff --git a/tests/unit/codegen/sdk/code_generation/test_api_doc_generation.py b/tests/unit/codegen/sdk/code_generation/test_api_doc_generation.py index 0e1222c64..123db39b4 100644 --- a/tests/unit/codegen/sdk/code_generation/test_api_doc_generation.py +++ b/tests/unit/codegen/sdk/code_generation/test_api_doc_generation.py @@ -40,7 +40,7 @@ def test_api_doc_generation_sanity(codebase, language: ProgrammingLanguage) -> N other_lang = "TS" if language == ProgrammingLanguage.PYTHON else "Py" # =====[ Python ]===== docs = get_codegen_sdk_docs(language=language, codebase=codebase) - assert count_tokens(docs) < 50500 + assert count_tokens(docs) < 50700 assert f"{lang}Function" in docs assert f"{lang}Class" in docs assert f"{other_lang}Function" not in docs diff --git a/tests/unit/codegen/sdk/python/import_resolution/test_is_dynamic.py b/tests/unit/codegen/sdk/python/import_resolution/test_is_dynamic.py new file mode 100644 index 000000000..c31f55f22 --- /dev/null +++ b/tests/unit/codegen/sdk/python/import_resolution/test_is_dynamic.py @@ -0,0 +1,227 @@ +from codegen.sdk.codebase.factory.get_session import get_codebase_session +from codegen.sdk.enums import ProgrammingLanguage + + +def test_py_import_is_dynamic_in_function(tmpdir): + # language=python + content = """ + def my_function(): + import foo # Dynamic import inside function + from bar import baz # Another dynamic import + + import static_import # Static import at module level + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + # Dynamic imports inside function + assert imports[0].is_dynamic # import foo + assert imports[1].is_dynamic # from bar import baz + + # Static import at module level + assert not imports[2].is_dynamic # import static_import + + +def test_py_import_is_dynamic_in_if_block(tmpdir): + # language=python + content = """ + import top_level # Static import + + if condition: + import conditional # Dynamic import in if block + from x import y # Another dynamic import + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # top_level import + assert imports[1].is_dynamic # conditional import + assert imports[2].is_dynamic # from x import y + + +def test_py_import_is_dynamic_in_try_except(tmpdir): + # language=python + content = """ + import static_first # Static import + + try: + import dynamic_in_try # Dynamic import in try block + from x.y import z # Another dynamic import + except ImportError: + pass + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_first import + assert imports[1].is_dynamic # dynamic_in_try import + assert imports[2].is_dynamic # from x.y import z + + +def test_py_import_is_dynamic_in_with_block(tmpdir): + # language=python + content = """ + import static_import # Static import + + with context_manager(): + import dynamic_in_with # Dynamic import in with block + from a.b import c # Another dynamic import + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_import + assert imports[1].is_dynamic # dynamic_in_with import + assert imports[2].is_dynamic # from a.b import c + + +def test_py_import_is_dynamic_in_class_method(tmpdir): + # language=python + content = """ + import static_import # Static import + + class MyClass: + def my_method(self): + import dynamic_in_method # Dynamic import in method + from pkg import module # Another dynamic import + + @classmethod + def class_method(cls): + import another_dynamic # Dynamic import in classmethod + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_import + assert imports[1].is_dynamic # dynamic_in_method import + assert imports[2].is_dynamic # from pkg import module + assert imports[3].is_dynamic # another_dynamic import + + +def test_py_import_is_dynamic_in_nested_function(tmpdir): + # language=python + content = """ + import static_import # Static import + + def outer_function(): + import dynamic_in_outer # Dynamic import in outer function + + def inner_function(): + import dynamic_in_inner # Dynamic import in inner function + from x import y # Another dynamic import + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_import + assert imports[1].is_dynamic # dynamic_in_outer import + assert imports[2].is_dynamic # dynamic_in_inner import + assert imports[3].is_dynamic # from x import y + + +def test_py_import_is_dynamic_in_else_clause(tmpdir): + # language=python + content = """ + import static_import # Static import + + if condition: + pass + else: + import dynamic_in_else # Dynamic import in else clause + from x import y # Another dynamic import + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_import + assert imports[1].is_dynamic # dynamic_in_else import + assert imports[2].is_dynamic # from x import y + + +def test_py_import_is_dynamic_in_except_clause(tmpdir): + # language=python + content = """ + import static_import # Static import + + try: + pass + except ImportError: + import dynamic_in_except # Dynamic import in except clause + from x import y # Another dynamic import + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_import + assert imports[1].is_dynamic # dynamic_in_except import + assert imports[2].is_dynamic # from x import y + + +def test_py_import_is_dynamic_in_finally_clause(tmpdir): + # language=python + content = """ + import static_import # Static import + + try: + pass + except ImportError: + pass + finally: + import dynamic_in_finally # Dynamic import in finally clause + from x import y # Another dynamic import + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_import + assert imports[1].is_dynamic # dynamic_in_finally import + assert imports[2].is_dynamic # from x import y + + +def test_py_import_is_dynamic_in_while_statement(tmpdir): + # language=python + content = """ + import static_import # Static import + + while condition: + import dynamic_in_while # Dynamic import in while loop + from a import b # Another dynamic import + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_import + assert imports[1].is_dynamic # dynamic_in_while import + assert imports[2].is_dynamic # from a import b + + +def test_py_import_is_dynamic_in_match_case(tmpdir): + # language=python + content = """ + import static_import # Static import + + match value: + case 1: + import dynamic_in_case # Dynamic import in case clause + from x import y # Another dynamic import + case _: + import another_dynamic # Dynamic import in default case + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase: + file = codebase.get_file("test.py") + imports = file.imports + + assert not imports[0].is_dynamic # static_import + assert imports[1].is_dynamic # dynamic_in_case import + assert imports[2].is_dynamic # from x import y + assert imports[3].is_dynamic # another_dynamic import diff --git a/tests/unit/codegen/sdk/typescript/import_resolution/test_is_dynamic.py b/tests/unit/codegen/sdk/typescript/import_resolution/test_is_dynamic.py new file mode 100644 index 000000000..33527dab5 --- /dev/null +++ b/tests/unit/codegen/sdk/typescript/import_resolution/test_is_dynamic.py @@ -0,0 +1,240 @@ +from codegen.sdk.codebase.factory.get_session import get_codebase_session +from codegen.sdk.enums import ProgrammingLanguage + + +def test_ts_import_is_dynamic_in_function_declaration(tmpdir): + # language=typescript + content = """ + import { staticImport } from './static'; + + function loadModule() { + import('./dynamic').then(module => { + console.log(module); + }); + } + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.ts": content}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase: + file = codebase.get_file("test.ts") + imports = file.imports + + assert not imports[0].is_dynamic # static import + assert imports[1].is_dynamic # dynamic import in function + + +def test_ts_import_is_dynamic_in_method_definition(tmpdir): + # language=typescript + content = """ + import { Component } from '@angular/core'; + + class MyComponent { + async loadFeature() { + const feature = await import('./feature'); + } + + @Decorator() + async decoratedMethod() { + const module = await import('./decorated'); + } + } + """ + with get_codebase_session(tmpdir=tmpdir, files={"test.ts": content}, programming_language=ProgrammingLanguage.TYPESCRIPT) as codebase: + file = codebase.get_file("test.ts") + imports = file.imports + + assert not imports[0].is_dynamic # static import + assert imports[1].is_dynamic # dynamic import in method + assert imports[2].is_dynamic # dynamic import in decorated method + + +def test_ts_import_is_dynamic_in_arrow_function(tmpdir): + # language=typescript + content = """ + import { useState } from 'react'; + + const MyComponent = () => { + const loadModule = async () => { + const module = await import('./lazy'); + }; + + return