codeflash/tests/test_languages/test_import_resolver.py
Sarthak Agarwal fa56eb7abe refactor
2026-02-11 02:05:54 +05:30

719 lines
25 KiB
Python

"""Tests for JavaScript/TypeScript import resolver.
These tests verify that the ImportResolver correctly resolves import paths
to actual file paths, enabling multi-file context extraction.
"""
import pytest
from codeflash.languages.javascript.import_resolver import HelperSearchContext, ImportResolver, MultiFileHelperFinder
from codeflash.languages.javascript.treesitter import ImportInfo
class TestImportResolver:
"""Tests for ImportResolver class."""
@pytest.fixture
def project_root(self, tmp_path):
"""Create a temporary project structure."""
# Create directories
src_dir = tmp_path / "src"
src_dir.mkdir()
lib_dir = src_dir / "lib"
lib_dir.mkdir()
utils_dir = src_dir / "utils"
utils_dir.mkdir()
# Create some test files
(src_dir / "main.ts").write_text("export function main() {}")
(src_dir / "helper.ts").write_text("export function helper() {}")
(lib_dir / "math.ts").write_text("export function add() {}")
(utils_dir / "index.ts").write_text("export function util() {}")
return tmp_path
@pytest.fixture
def resolver(self, project_root):
"""Create an ImportResolver for the project."""
return ImportResolver(project_root)
def test_is_external_package_lodash(self, resolver):
"""Test that bare imports are detected as external."""
assert resolver._is_external_package("lodash") is True
def test_is_external_package_scoped(self, resolver):
"""Test that scoped packages are detected as external."""
assert resolver._is_external_package("@company/utils") is True
def test_is_external_package_react(self, resolver):
"""Test that react is detected as external."""
assert resolver._is_external_package("react") is True
def test_is_not_external_package_relative(self, resolver):
"""Test that relative imports are not external."""
assert resolver._is_external_package("./utils") is False
def test_is_not_external_package_parent_relative(self, resolver):
"""Test that parent relative imports are not external."""
assert resolver._is_external_package("../lib/math") is False
def test_resolve_relative_import_same_dir(self, resolver, project_root):
"""Test resolving ./helper from same directory."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="./helper",
default_import=None,
named_imports=[("helper", None)],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is not None
assert result.file_path == project_root / "src" / "helper.ts"
assert result.module_path == "./helper"
def test_resolve_relative_import_subdirectory(self, resolver, project_root):
"""Test resolving ./lib/math from parent directory."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="./lib/math",
default_import=None,
named_imports=[("add", None)],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is not None
assert result.file_path == project_root / "src" / "lib" / "math.ts"
def test_resolve_index_file(self, resolver, project_root):
"""Test resolving ./utils to ./utils/index.ts."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="./utils",
default_import=None,
named_imports=[("util", None)],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is not None
assert result.file_path == project_root / "src" / "utils" / "index.ts"
def test_resolve_external_package_returns_none(self, resolver, project_root):
"""Test that external package imports return None."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="lodash",
default_import="_",
named_imports=[],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is None
def test_resolve_nonexistent_file_returns_none(self, resolver, project_root):
"""Test that nonexistent file imports return None."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="./nonexistent",
default_import=None,
named_imports=[("foo", None)],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is None
def test_resolve_with_explicit_extension(self, resolver, project_root):
"""Test resolving import with explicit .ts extension."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="./helper.ts",
default_import=None,
named_imports=[("helper", None)],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is not None
assert result.file_path == project_root / "src" / "helper.ts"
def test_resolved_import_contains_imported_names(self, resolver, project_root):
"""Test that ResolvedImport contains correct imported names."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="./helper",
default_import="Helper",
named_imports=[("foo", None), ("bar", "baz")],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is not None
assert "Helper" in result.imported_names
assert "foo" in result.imported_names
assert "baz" in result.imported_names # alias is used
assert result.is_default_import is True
def test_namespace_import_detection(self, resolver, project_root):
"""Test that namespace imports are correctly detected."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="./helper",
default_import=None,
named_imports=[],
namespace_import="utils",
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is not None
assert result.is_namespace_import is True
assert result.namespace_name == "utils"
def test_caching_works(self, resolver, project_root):
"""Test that resolution results are cached."""
source_file = project_root / "src" / "main.ts"
import_info = ImportInfo(
module_path="./helper",
default_import=None,
named_imports=[("helper", None)],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
# First resolution
result1 = resolver.resolve_import(import_info, source_file)
# Second resolution should use cache
result2 = resolver.resolve_import(import_info, source_file)
assert result1 is not None
assert result2 is not None
assert result1.file_path == result2.file_path
# Check cache was populated
assert (source_file, "./helper") in resolver._resolution_cache
class TestMultiFileHelperFinder:
"""Tests for MultiFileHelperFinder class."""
@pytest.fixture
def project_root(self, tmp_path):
"""Create a temporary project with multi-file structure."""
src_dir = tmp_path / "src"
src_dir.mkdir()
# Main file that imports helper
(src_dir / "main.ts").write_text("""
import { helperFunc } from './helper';
export function mainFunc() {
return helperFunc() + 1;
}
""")
# Helper file
(src_dir / "helper.ts").write_text("""
export function helperFunc() {
return 42;
}
export function unusedHelper() {
return 0;
}
""")
return tmp_path
@pytest.fixture
def resolver(self, project_root):
"""Create an ImportResolver."""
return ImportResolver(project_root)
@pytest.fixture
def finder(self, project_root, resolver):
"""Create a MultiFileHelperFinder."""
return MultiFileHelperFinder(project_root, resolver)
def test_helper_search_context_defaults(self):
"""Test HelperSearchContext default values."""
context = HelperSearchContext()
assert context.visited_files == set()
assert context.visited_functions == set()
assert context.current_depth == 0
assert context.max_depth == 2
class TestExportInfo:
"""Tests for ExportInfo parsing in TreeSitterAnalyzer."""
@pytest.fixture
def js_analyzer(self):
"""Create a JavaScript analyzer."""
from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage
return TreeSitterAnalyzer(TreeSitterLanguage.JAVASCRIPT)
def test_find_named_export_function(self, js_analyzer):
"""Test finding export function declaration."""
code = "export function helper() { return 1; }"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert ("helper", None) in exports[0].exported_names
assert exports[0].is_reexport is False
def test_find_default_export_function(self, js_analyzer):
"""Test finding export default function."""
code = "export default function myFunc() { return 1; }"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert exports[0].default_export == "myFunc"
def test_find_export_declaration(self, js_analyzer):
"""Test finding export { name }."""
code = """
function helper() { return 1; }
export { helper };
"""
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert ("helper", None) in exports[0].exported_names
def test_find_export_with_alias(self, js_analyzer):
"""Test finding export { name as alias }."""
code = """
function helper() { return 1; }
export { helper as myHelper };
"""
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert ("helper", "myHelper") in exports[0].exported_names
def test_find_reexport(self, js_analyzer):
"""Test finding re-export from another module."""
code = "export { helper } from './other';"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert exports[0].is_reexport is True
assert exports[0].reexport_source == "./other"
def test_find_export_const(self, js_analyzer):
"""Test finding export const declaration."""
code = "export const myVar = 42;"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert ("myVar", None) in exports[0].exported_names
def test_is_function_exported_true(self, js_analyzer):
"""Test is_function_exported returns True for exported function."""
code = "export function helper() { return 1; }"
is_exported, export_name = js_analyzer.is_function_exported(code, "helper")
assert is_exported is True
assert export_name == "helper"
def test_is_function_exported_false(self, js_analyzer):
"""Test is_function_exported returns False for non-exported function."""
code = "function helper() { return 1; }"
is_exported, export_name = js_analyzer.is_function_exported(code, "helper")
assert is_exported is False
assert export_name is None
def test_is_function_exported_with_alias(self, js_analyzer):
"""Test is_function_exported returns alias name."""
code = """
function helper() { return 1; }
export { helper as myHelper };
"""
is_exported, export_name = js_analyzer.is_function_exported(code, "helper")
assert is_exported is True
assert export_name == "myHelper"
def test_is_function_exported_default(self, js_analyzer):
"""Test is_function_exported returns 'default' for default export."""
code = "export default function helper() { return 1; }"
is_exported, export_name = js_analyzer.is_function_exported(code, "helper")
assert is_exported is True
assert export_name == "default"
class TestCommonJSRequire:
"""Tests for CommonJS require() import parsing."""
@pytest.fixture
def js_analyzer(self):
"""Create a JavaScript analyzer."""
from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage
return TreeSitterAnalyzer(TreeSitterLanguage.JAVASCRIPT)
def test_require_default_import(self, js_analyzer):
"""Test const foo = require('./module')."""
code = "const helper = require('./helper');"
imports = js_analyzer.find_imports(code)
assert len(imports) == 1
assert imports[0].module_path == "./helper"
assert imports[0].default_import == "helper"
assert imports[0].named_imports == []
def test_require_destructured_import(self, js_analyzer):
"""Test const { a, b } = require('./module')."""
code = "const { foo, bar } = require('./helper');"
imports = js_analyzer.find_imports(code)
assert len(imports) == 1
assert imports[0].module_path == "./helper"
assert imports[0].default_import is None
assert ("foo", None) in imports[0].named_imports
assert ("bar", None) in imports[0].named_imports
def test_require_destructured_with_alias(self, js_analyzer):
"""Test const { a: aliasA } = require('./module')."""
code = "const { foo: myFoo, bar } = require('./helper');"
imports = js_analyzer.find_imports(code)
assert len(imports) == 1
assert imports[0].module_path == "./helper"
assert ("foo", "myFoo") in imports[0].named_imports
assert ("bar", None) in imports[0].named_imports
def test_require_property_access(self, js_analyzer):
"""Test const foo = require('./module').bar."""
code = "const myFunc = require('./helper').helperFunc;"
imports = js_analyzer.find_imports(code)
assert len(imports) == 1
assert imports[0].module_path == "./helper"
assert imports[0].default_import is None
# helperFunc is imported and assigned to myFunc
assert ("helperFunc", "myFunc") in imports[0].named_imports
def test_require_property_access_same_name(self, js_analyzer):
"""Test const foo = require('./module').foo."""
code = "const helperFunc = require('./helper').helperFunc;"
imports = js_analyzer.find_imports(code)
assert len(imports) == 1
assert imports[0].module_path == "./helper"
# When var name equals property, no alias needed
assert ("helperFunc", None) in imports[0].named_imports
def test_require_external_package(self, js_analyzer):
"""Test require for external packages."""
code = "const lodash = require('lodash');"
imports = js_analyzer.find_imports(code)
assert len(imports) == 1
assert imports[0].module_path == "lodash"
assert imports[0].default_import == "lodash"
def test_require_side_effect_import(self, js_analyzer):
"""Test require('./module') without assignment."""
code = "require('./side-effects');"
imports = js_analyzer.find_imports(code)
assert len(imports) == 1
assert imports[0].module_path == "./side-effects"
assert imports[0].default_import is None
assert imports[0].named_imports == []
class TestCommonJSExports:
"""Tests for CommonJS module.exports parsing."""
@pytest.fixture
def js_analyzer(self):
"""Create a JavaScript analyzer."""
from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage
return TreeSitterAnalyzer(TreeSitterLanguage.JAVASCRIPT)
@pytest.fixture
def ts_analyzer(self):
"""Create a TypeScript analyzer."""
from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage
return TreeSitterAnalyzer(TreeSitterLanguage.TYPESCRIPT)
def test_module_exports_function(self, js_analyzer):
"""Test module.exports = function() {}."""
code = "module.exports = function helper() { return 1; };"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert exports[0].default_export == "helper"
def test_module_exports_anonymous_function(self, js_analyzer):
"""Test module.exports = function() {} (anonymous)."""
code = "module.exports = function() { return 1; };"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert exports[0].default_export == "default"
def test_module_exports_arrow_function(self, js_analyzer):
"""Test module.exports = () => {}."""
code = "module.exports = () => { return 1; };"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert exports[0].default_export == "default"
def test_module_exports_identifier(self, js_analyzer):
"""Test module.exports = existingFunction."""
code = """
function helper() { return 1; }
module.exports = helper;
"""
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert exports[0].default_export == "helper"
def test_module_exports_object(self, js_analyzer):
"""Test module.exports = { foo, bar }."""
code = """
function foo() {}
function bar() {}
module.exports = { foo, bar };
"""
exports = js_analyzer.find_exports(code)
# Should find the module.exports object
module_export = [e for e in exports if e.exported_names]
assert len(module_export) == 1
assert ("foo", None) in module_export[0].exported_names
assert ("bar", None) in module_export[0].exported_names
def test_module_exports_object_with_rename(self, js_analyzer):
"""Test module.exports = { publicName: localFunc }."""
code = """
function helper() {}
module.exports = { publicHelper: helper };
"""
exports = js_analyzer.find_exports(code)
module_export = [e for e in exports if e.exported_names]
assert len(module_export) == 1
# helper is exported as publicHelper
assert ("helper", "publicHelper") in module_export[0].exported_names
def test_module_exports_property(self, js_analyzer):
"""Test module.exports.foo = function() {}."""
code = "module.exports.helper = function() { return 1; };"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert ("helper", None) in exports[0].exported_names
def test_exports_property(self, js_analyzer):
"""Test exports.foo = function() {}."""
code = "exports.helper = function() { return 1; };"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert ("helper", None) in exports[0].exported_names
def test_module_exports_require_reexport(self, js_analyzer):
"""Test module.exports = require('./other')."""
code = "module.exports = require('./other');"
exports = js_analyzer.find_exports(code)
assert len(exports) == 1
assert exports[0].is_reexport is True
assert exports[0].reexport_source == "./other"
def test_is_function_exported_commonjs(self, js_analyzer):
"""Test is_function_exported works with CommonJS exports."""
code = """
function helper() { return 1; }
module.exports = { helper };
"""
is_exported, export_name = js_analyzer.is_function_exported(code, "helper")
assert is_exported is True
assert export_name == "helper"
def test_is_function_exported_commonjs_property(self, js_analyzer):
"""Test is_function_exported with exports.foo pattern."""
code = """
function helper() { return 1; }
exports.helper = helper;
"""
is_exported, export_name = js_analyzer.is_function_exported(code, "helper")
assert is_exported is True
assert export_name == "helper"
def test_is_class_method_exported_via_class(self, ts_analyzer):
"""Test is_function_exported returns True for method of exported class."""
code = """
export class BloomFilter {
getHashValues(key: string): number[] {
return [1, 2, 3];
}
}
"""
# Method itself is not directly exported
is_exported, export_name = ts_analyzer.is_function_exported(code, "getHashValues")
assert is_exported is False
assert export_name is None
# But when we pass the class name, it should find the class export
is_exported, export_name = ts_analyzer.is_function_exported(code, "getHashValues", "BloomFilter")
assert is_exported is True
assert export_name == "BloomFilter"
def test_is_class_method_exported_default_class(self, ts_analyzer):
"""Test is_function_exported returns True for method of default exported class."""
code = """
export default class Calculator {
add(a: number, b: number): number {
return a + b;
}
}
"""
# When we pass the class name, it should find the default export
is_exported, export_name = ts_analyzer.is_function_exported(code, "add", "Calculator")
assert is_exported is True
assert export_name == "Calculator"
def test_is_class_method_not_exported_non_exported_class(self, ts_analyzer):
"""Test is_function_exported returns False for method of non-exported class."""
code = """
class InternalClass {
helper(): void {}
}
"""
# Even with class name, non-exported class method should not be exported
is_exported, export_name = ts_analyzer.is_function_exported(code, "helper", "InternalClass")
assert is_exported is False
assert export_name is None
class TestCommonJSImportResolver:
"""Tests for ImportResolver with CommonJS require() imports."""
@pytest.fixture
def project_root(self, tmp_path):
"""Create a temporary project structure with CommonJS files."""
src_dir = tmp_path / "src"
src_dir.mkdir()
# Create CommonJS module files
(src_dir / "main.js").write_text("""
const helper = require('./helper');
const { add, subtract } = require('./math');
function main() {
return helper.process() + add(1, 2);
}
module.exports = main;
""")
(src_dir / "helper.js").write_text("""
function process() {
return 42;
}
module.exports = { process };
""")
(src_dir / "math.js").write_text("""
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
module.exports = { add, subtract };
""")
return tmp_path
@pytest.fixture
def resolver(self, project_root):
"""Create an ImportResolver for the project."""
return ImportResolver(project_root)
def test_resolve_commonjs_default_require(self, resolver, project_root):
"""Test resolving const foo = require('./module')."""
source_file = project_root / "src" / "main.js"
import_info = ImportInfo(
module_path="./helper",
default_import="helper",
named_imports=[],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is not None
assert result.file_path == project_root / "src" / "helper.js"
def test_resolve_commonjs_destructured_require(self, resolver, project_root):
"""Test resolving const { a, b } = require('./module')."""
source_file = project_root / "src" / "main.js"
import_info = ImportInfo(
module_path="./math",
default_import=None,
named_imports=[("add", None), ("subtract", None)],
namespace_import=None,
is_type_only=False,
start_line=1,
end_line=1,
)
result = resolver.resolve_import(import_info, source_file)
assert result is not None
assert result.file_path == project_root / "src" / "math.js"
assert "add" in result.imported_names
assert "subtract" in result.imported_names