1103 lines
39 KiB
Python
1103 lines
39 KiB
Python
"""Comprehensive tests for JavaScript/TypeScript find references functionality.
|
|
|
|
These tests are inspired by real-world patterns found in the Appsmith codebase,
|
|
covering various import/export patterns, callback usage, memoization, and more.
|
|
|
|
Each test verifies:
|
|
1. The actual reference values (file, line, column, type, caller)
|
|
2. The formatted markdown output from _format_references_as_markdown
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
|
|
from codeflash.languages.javascript.find_references import (
|
|
Reference,
|
|
ReferenceFinder,
|
|
ExportedFunction,
|
|
ReferenceSearchContext,
|
|
find_references,
|
|
)
|
|
from codeflash.languages.base import Language, FunctionInfo, ReferenceInfo
|
|
from codeflash.languages.python.static_analysis.code_extractor import _format_references_as_markdown
|
|
from codeflash.models.models import FunctionParent
|
|
|
|
|
|
def make_func(name: str, file_path: Path, class_name: str | None = None) -> FunctionToOptimize:
|
|
"""Helper to create FunctionToOptimize for testing."""
|
|
parents = [FunctionParent(name=class_name, type="ClassDef")] if class_name else []
|
|
return FunctionToOptimize(
|
|
function_name=name,
|
|
file_path=file_path,
|
|
parents=parents,
|
|
language="javascript",
|
|
)
|
|
|
|
|
|
class TestReferenceFinder:
|
|
"""Tests for ReferenceFinder class."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create a basic project structure."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
return tmp_path
|
|
|
|
@pytest.fixture
|
|
def finder(self, project_root):
|
|
"""Create a ReferenceFinder instance."""
|
|
return ReferenceFinder(project_root)
|
|
|
|
def test_init_default_exclude_patterns(self, project_root):
|
|
"""Test that default exclude patterns are set."""
|
|
finder = ReferenceFinder(project_root)
|
|
assert "node_modules" in finder.exclude_patterns
|
|
assert "dist" in finder.exclude_patterns
|
|
assert ".git" in finder.exclude_patterns
|
|
|
|
def test_init_custom_exclude_patterns(self, project_root):
|
|
"""Test custom exclude patterns."""
|
|
finder = ReferenceFinder(project_root, exclude_patterns=["custom_dir"])
|
|
assert "custom_dir" in finder.exclude_patterns
|
|
assert "node_modules" not in finder.exclude_patterns
|
|
|
|
def test_should_exclude_node_modules(self, finder, project_root):
|
|
"""Test that node_modules files are excluded."""
|
|
path = project_root / "node_modules" / "lodash" / "index.js"
|
|
assert finder._should_exclude(path) is True
|
|
|
|
def test_should_not_exclude_src(self, finder, project_root):
|
|
"""Test that src files are not excluded."""
|
|
path = project_root / "src" / "utils.ts"
|
|
assert finder._should_exclude(path) is False
|
|
|
|
|
|
class TestBasicNamedExports:
|
|
"""Tests for basic named export/import patterns.
|
|
|
|
Inspired by Appsmith patterns like:
|
|
import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils";
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with named export pattern."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
utils_dir = src_dir / "utils"
|
|
utils_dir.mkdir()
|
|
|
|
# Source file with named export
|
|
(utils_dir / "DynamicBindingUtils.ts").write_text(
|
|
'export function getDynamicBindings(value: string): string[] {\n'
|
|
' const regex = /{{([^}]+)}}/g;\n'
|
|
' return [];\n'
|
|
'}\n'
|
|
)
|
|
|
|
# File that imports and uses the function
|
|
(src_dir / "evaluator.ts").write_text(
|
|
"import { getDynamicBindings } from './utils/DynamicBindingUtils';\n"
|
|
'\n'
|
|
'export function evaluate(expression: string) {\n'
|
|
' const bindings = getDynamicBindings(expression);\n'
|
|
' return bindings;\n'
|
|
'}\n'
|
|
)
|
|
|
|
# Another file that uses the function
|
|
(src_dir / "validator.ts").write_text(
|
|
"import { getDynamicBindings } from './utils/DynamicBindingUtils';\n"
|
|
'\n'
|
|
'export function validateBindings(input: string) {\n'
|
|
' const bindings = getDynamicBindings(input);\n'
|
|
' return bindings.length > 0;\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_named_export_references_values(self, project_root):
|
|
"""Test finding references to a named exported function with exact values."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "utils" / "DynamicBindingUtils.ts"
|
|
|
|
refs = finder.find_references(make_func("getDynamicBindings", source_file))
|
|
|
|
# Sort refs by file path for consistent ordering
|
|
refs_sorted = sorted(refs, key=lambda r: (str(r.file_path), r.line))
|
|
|
|
# Should find 2 references
|
|
assert len(refs_sorted) == 2
|
|
|
|
# Check evaluator.ts reference
|
|
eval_ref = next(r for r in refs_sorted if "evaluator.ts" in str(r.file_path))
|
|
assert eval_ref.line == 4
|
|
assert eval_ref.reference_type == "call"
|
|
assert eval_ref.caller_function == "evaluate"
|
|
assert eval_ref.import_name == "getDynamicBindings"
|
|
assert "getDynamicBindings(expression)" in eval_ref.context
|
|
|
|
# Check validator.ts reference
|
|
val_ref = next(r for r in refs_sorted if "validator.ts" in str(r.file_path))
|
|
assert val_ref.line == 4
|
|
assert val_ref.reference_type == "call"
|
|
assert val_ref.caller_function == "validateBindings"
|
|
assert val_ref.import_name == "getDynamicBindings"
|
|
assert "getDynamicBindings(input)" in val_ref.context
|
|
|
|
def test_format_references_as_markdown_named_exports(self, project_root):
|
|
"""Test _format_references_as_markdown output for named exports."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "utils" / "DynamicBindingUtils.ts"
|
|
|
|
refs = finder.find_references(make_func("getDynamicBindings", source_file))
|
|
|
|
# Convert to ReferenceInfo and sort for consistent ordering
|
|
ref_infos = sorted([
|
|
ReferenceInfo(
|
|
file_path=r.file_path,
|
|
line=r.line,
|
|
column=r.column,
|
|
end_line=r.end_line,
|
|
end_column=r.end_column,
|
|
context=r.context,
|
|
reference_type=r.reference_type,
|
|
import_name=r.import_name,
|
|
caller_function=r.caller_function,
|
|
)
|
|
for r in refs
|
|
], key=lambda r: str(r.file_path))
|
|
|
|
markdown = _format_references_as_markdown(ref_infos, source_file, project_root, Language.TYPESCRIPT)
|
|
|
|
expected_markdown = (
|
|
'```typescript:src/evaluator.ts\n'
|
|
'function evaluate(expression: string) {\n'
|
|
' const bindings = getDynamicBindings(expression);\n'
|
|
' return bindings;\n'
|
|
'}\n'
|
|
'```\n'
|
|
'```typescript:src/validator.ts\n'
|
|
'function validateBindings(input: string) {\n'
|
|
' const bindings = getDynamicBindings(input);\n'
|
|
' return bindings.length > 0;\n'
|
|
'}\n'
|
|
'```\n'
|
|
)
|
|
assert markdown == expected_markdown
|
|
|
|
|
|
class TestDefaultExports:
|
|
"""Tests for default export/import patterns."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with default export pattern."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
|
|
# Source file with default export
|
|
(src_dir / "helper.ts").write_text(
|
|
'function processData(data: any[]) {\n'
|
|
' return data.filter(item => item.active);\n'
|
|
'}\n'
|
|
'\n'
|
|
'export default processData;\n'
|
|
)
|
|
|
|
# File that imports the default export
|
|
(src_dir / "main.ts").write_text(
|
|
"import processData from './helper';\n"
|
|
'\n'
|
|
'export function handleData(items: any[]) {\n'
|
|
' const processed = processData(items);\n'
|
|
' return processed.length;\n'
|
|
'}\n'
|
|
)
|
|
|
|
# File that imports with a different name
|
|
(src_dir / "alternative.ts").write_text(
|
|
"import myProcessor from './helper';\n"
|
|
'\n'
|
|
'export function process(items: any[]) {\n'
|
|
' return myProcessor(items);\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_default_export_references_values(self, project_root):
|
|
"""Test finding references to a default exported function with exact values."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "helper.ts"
|
|
|
|
refs = finder.find_references(make_func("processData", source_file))
|
|
|
|
# Should find references in both files
|
|
ref_files = {str(ref.file_path) for ref in refs}
|
|
assert any("main.ts" in f for f in ref_files)
|
|
assert any("alternative.ts" in f for f in ref_files)
|
|
|
|
# Check main.ts reference (uses original name)
|
|
main_ref = next(r for r in refs if "main.ts" in str(r.file_path))
|
|
assert main_ref.line == 4
|
|
assert main_ref.reference_type == "call"
|
|
assert main_ref.caller_function == "handleData"
|
|
assert main_ref.import_name == "processData"
|
|
|
|
# Check alternative.ts reference (uses alias)
|
|
alt_ref = next(r for r in refs if "alternative.ts" in str(r.file_path))
|
|
assert alt_ref.line == 4
|
|
assert alt_ref.reference_type == "call"
|
|
assert alt_ref.caller_function == "process"
|
|
assert alt_ref.import_name == "myProcessor"
|
|
|
|
def test_format_references_as_markdown_default_exports(self, project_root):
|
|
"""Test markdown output for default exports with aliases."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "helper.ts"
|
|
|
|
refs = finder.find_references(make_func("processData", source_file))
|
|
ref_infos = sorted([
|
|
ReferenceInfo(
|
|
file_path=r.file_path, line=r.line, column=r.column,
|
|
end_line=r.end_line, end_column=r.end_column, context=r.context,
|
|
reference_type=r.reference_type, import_name=r.import_name,
|
|
caller_function=r.caller_function,
|
|
)
|
|
for r in refs
|
|
], key=lambda r: str(r.file_path))
|
|
|
|
markdown = _format_references_as_markdown(ref_infos, source_file, project_root, Language.TYPESCRIPT)
|
|
|
|
expected_markdown = (
|
|
'```typescript:src/alternative.ts\n'
|
|
'function process(items: any[]) {\n'
|
|
' return myProcessor(items);\n'
|
|
'}\n'
|
|
'```\n'
|
|
'```typescript:src/main.ts\n'
|
|
'function handleData(items: any[]) {\n'
|
|
' const processed = processData(items);\n'
|
|
' return processed.length;\n'
|
|
'}\n'
|
|
'```\n'
|
|
)
|
|
assert markdown == expected_markdown
|
|
|
|
|
|
class TestReExports:
|
|
"""Tests for re-export patterns."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with re-export pattern."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
utils_dir = src_dir / "utils"
|
|
utils_dir.mkdir()
|
|
|
|
# Original function file
|
|
(utils_dir / "filterUtils.ts").write_text(
|
|
'export function filterBySearchTerm(items: any[], term: string) {\n'
|
|
' return items.filter(i => i.name.includes(term));\n'
|
|
'}\n'
|
|
)
|
|
|
|
# Index file that re-exports
|
|
(utils_dir / "index.ts").write_text(
|
|
"export { filterBySearchTerm } from './filterUtils';\n"
|
|
)
|
|
|
|
# Consumer that imports from index
|
|
(src_dir / "consumer.ts").write_text(
|
|
"import { filterBySearchTerm } from './utils';\n"
|
|
'\n'
|
|
'export function searchItems(items: any[], query: string) {\n'
|
|
' return filterBySearchTerm(items, query);\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_reexport_reference_values(self, project_root):
|
|
"""Test finding re-export references with exact values."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "utils" / "filterUtils.ts"
|
|
|
|
refs = finder.find_references(make_func("filterBySearchTerm", source_file))
|
|
|
|
# Should find re-export in index.ts
|
|
reexport_refs = [r for r in refs if r.reference_type == "reexport"]
|
|
assert len(reexport_refs) == 1
|
|
assert "index.ts" in str(reexport_refs[0].file_path)
|
|
assert reexport_refs[0].import_name == "filterBySearchTerm"
|
|
|
|
# Should find call in consumer.ts (through re-export chain)
|
|
call_refs = [r for r in refs if r.reference_type == "call"]
|
|
assert len(call_refs) >= 1
|
|
consumer_ref = next((r for r in call_refs if "consumer.ts" in str(r.file_path)), None)
|
|
assert consumer_ref is not None
|
|
assert consumer_ref.line == 4
|
|
assert consumer_ref.caller_function == "searchItems"
|
|
|
|
def test_format_references_as_markdown_reexports(self, project_root):
|
|
"""Test markdown output for re-exports."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "utils" / "filterUtils.ts"
|
|
|
|
refs = finder.find_references(make_func("filterBySearchTerm", source_file))
|
|
ref_infos = sorted([
|
|
ReferenceInfo(
|
|
file_path=r.file_path, line=r.line, column=r.column,
|
|
end_line=r.end_line, end_column=r.end_column, context=r.context,
|
|
reference_type=r.reference_type, import_name=r.import_name,
|
|
caller_function=r.caller_function,
|
|
)
|
|
for r in refs
|
|
], key=lambda r: str(r.file_path))
|
|
|
|
markdown = _format_references_as_markdown(ref_infos, source_file, project_root, Language.TYPESCRIPT)
|
|
|
|
expected_markdown = (
|
|
'```typescript:src/consumer.ts\n'
|
|
'function searchItems(items: any[], query: string) {\n'
|
|
' return filterBySearchTerm(items, query);\n'
|
|
'}\n'
|
|
'```\n'
|
|
'```typescript:src/utils/index.ts\n'
|
|
"export { filterBySearchTerm } from './filterUtils';\n"
|
|
'```\n'
|
|
)
|
|
assert markdown == expected_markdown
|
|
|
|
|
|
class TestCallbackPatterns:
|
|
"""Tests for functions passed as callbacks."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with callback patterns."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
|
|
# Helper function
|
|
(src_dir / "transforms.ts").write_text(
|
|
'export function normalizeItem(item: any) {\n'
|
|
' return { ...item, normalized: true };\n'
|
|
'}\n'
|
|
)
|
|
|
|
# Consumer using callbacks
|
|
(src_dir / "processor.ts").write_text(
|
|
"import { normalizeItem } from './transforms';\n"
|
|
'\n'
|
|
'export function processItems(items: any[]) {\n'
|
|
' const normalized = items.map(normalizeItem);\n'
|
|
' return normalized;\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_callback_references_values(self, project_root):
|
|
"""Test finding functions used as callbacks with exact values."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "transforms.ts"
|
|
|
|
refs = finder.find_references(make_func("normalizeItem", source_file))
|
|
|
|
# Should find the callback reference
|
|
callback_refs = [r for r in refs if r.reference_type == "callback"]
|
|
assert len(callback_refs) >= 1
|
|
|
|
callback_ref = callback_refs[0]
|
|
assert "processor.ts" in str(callback_ref.file_path)
|
|
assert callback_ref.line == 4
|
|
assert callback_ref.caller_function == "processItems"
|
|
assert "items.map(normalizeItem)" in callback_ref.context
|
|
|
|
def test_format_references_as_markdown_callbacks(self, project_root):
|
|
"""Test markdown output for callback patterns."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "transforms.ts"
|
|
|
|
refs = finder.find_references(make_func("normalizeItem", source_file))
|
|
ref_infos = [
|
|
ReferenceInfo(
|
|
file_path=r.file_path, line=r.line, column=r.column,
|
|
end_line=r.end_line, end_column=r.end_column, context=r.context,
|
|
reference_type=r.reference_type, import_name=r.import_name,
|
|
caller_function=r.caller_function,
|
|
)
|
|
for r in refs
|
|
]
|
|
|
|
markdown = _format_references_as_markdown(ref_infos, source_file, project_root, Language.TYPESCRIPT)
|
|
|
|
expected_markdown = (
|
|
'```typescript:src/processor.ts\n'
|
|
'function processItems(items: any[]) {\n'
|
|
' const normalized = items.map(normalizeItem);\n'
|
|
' return normalized;\n'
|
|
'}\n'
|
|
'```\n'
|
|
)
|
|
assert expected_markdown == markdown
|
|
|
|
|
|
class TestAliasImports:
|
|
"""Tests for functions imported with aliases."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with alias import patterns."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
|
|
# Source file
|
|
(src_dir / "utils.ts").write_text(
|
|
'export function computeValue(input: number): number {\n'
|
|
' return input * 2;\n'
|
|
'}\n'
|
|
)
|
|
|
|
# File using alias
|
|
(src_dir / "consumer.ts").write_text(
|
|
"import { computeValue as calculate } from './utils';\n"
|
|
'\n'
|
|
'export function processNumber(n: number) {\n'
|
|
' const result = calculate(n);\n'
|
|
' return result + 10;\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_aliased_import_references_values(self, project_root):
|
|
"""Test finding references when function is imported with alias."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "utils.ts"
|
|
|
|
refs = finder.find_references(make_func("computeValue", source_file))
|
|
|
|
# Should find the reference even though it's called as "calculate"
|
|
assert len(refs) == 1
|
|
ref = refs[0]
|
|
assert "consumer.ts" in str(ref.file_path)
|
|
assert ref.line == 4
|
|
assert ref.reference_type == "call"
|
|
assert ref.import_name == "calculate" # The aliased name
|
|
assert ref.caller_function == "processNumber"
|
|
assert "calculate(n)" in ref.context
|
|
|
|
def test_format_references_as_markdown_aliases(self, project_root):
|
|
"""Test markdown output for aliased imports."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "utils.ts"
|
|
|
|
refs = finder.find_references(make_func("computeValue", source_file))
|
|
ref_infos = [
|
|
ReferenceInfo(
|
|
file_path=r.file_path, line=r.line, column=r.column,
|
|
end_line=r.end_line, end_column=r.end_column, context=r.context,
|
|
reference_type=r.reference_type, import_name=r.import_name,
|
|
caller_function=r.caller_function,
|
|
)
|
|
for r in refs
|
|
]
|
|
|
|
markdown = _format_references_as_markdown(ref_infos, source_file, project_root, Language.TYPESCRIPT)
|
|
|
|
expected_markdown = (
|
|
'```typescript:src/consumer.ts\n'
|
|
'function processNumber(n: number) {\n'
|
|
' const result = calculate(n);\n'
|
|
' return result + 10;\n'
|
|
'}\n'
|
|
'```\n'
|
|
)
|
|
assert expected_markdown == markdown
|
|
|
|
|
|
class TestNamespaceImports:
|
|
"""Tests for namespace import patterns."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with namespace import patterns."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
|
|
# Source file with multiple exports
|
|
(src_dir / "mathUtils.ts").write_text(
|
|
'export function add(a: number, b: number): number {\n'
|
|
' return a + b;\n'
|
|
'}\n'
|
|
)
|
|
|
|
# File using namespace import
|
|
(src_dir / "calculator.ts").write_text(
|
|
"import * as MathUtils from './mathUtils';\n"
|
|
'\n'
|
|
'export function calculate(a: number, b: number) {\n'
|
|
' return MathUtils.add(a, b);\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_namespace_import_references_values(self, project_root):
|
|
"""Test finding references via namespace imports with exact values."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "mathUtils.ts"
|
|
|
|
refs = finder.find_references(make_func("add", source_file))
|
|
|
|
assert len(refs) == 1
|
|
ref = refs[0]
|
|
assert "calculator.ts" in str(ref.file_path)
|
|
assert ref.line == 4
|
|
assert ref.reference_type == "call"
|
|
assert ref.import_name == "MathUtils.add"
|
|
assert ref.caller_function == "calculate"
|
|
assert "MathUtils.add(a, b)" in ref.context
|
|
|
|
def test_format_references_as_markdown_namespace(self, project_root):
|
|
"""Test markdown output for namespace imports."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "mathUtils.ts"
|
|
|
|
refs = finder.find_references(make_func("add", source_file))
|
|
ref_infos = [
|
|
ReferenceInfo(
|
|
file_path=r.file_path, line=r.line, column=r.column,
|
|
end_line=r.end_line, end_column=r.end_column, context=r.context,
|
|
reference_type=r.reference_type, import_name=r.import_name,
|
|
caller_function=r.caller_function,
|
|
)
|
|
for r in refs
|
|
]
|
|
|
|
markdown = _format_references_as_markdown(ref_infos, source_file, project_root, Language.TYPESCRIPT)
|
|
|
|
expected_markdown = (
|
|
'```typescript:src/calculator.ts\n'
|
|
'function calculate(a: number, b: number) {\n'
|
|
' return MathUtils.add(a, b);\n'
|
|
'}\n'
|
|
'```\n'
|
|
)
|
|
assert expected_markdown == markdown
|
|
|
|
|
|
class TestMemoizedFunctions:
|
|
"""Tests for memoized function patterns."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with memoized function patterns."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
|
|
# Source file with function to be memoized
|
|
(src_dir / "expensive.ts").write_text(
|
|
'export function computeExpensive(x: number): number {\n'
|
|
' return x * x;\n'
|
|
'}\n'
|
|
)
|
|
|
|
# File that memoizes the function
|
|
(src_dir / "memoized.ts").write_text(
|
|
"import memoize from 'micro-memoize';\n"
|
|
"import { computeExpensive } from './expensive';\n"
|
|
'\n'
|
|
'export const memoizedCompute = memoize(computeExpensive);\n'
|
|
'\n'
|
|
'export function process(x: number) {\n'
|
|
' return computeExpensive(x) + memoizedCompute(x);\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_memoized_function_references_values(self, project_root):
|
|
"""Test finding references to functions passed to memoize."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "expensive.ts"
|
|
|
|
refs = finder.find_references(make_func("computeExpensive", source_file))
|
|
|
|
# Should find memoize call and direct call
|
|
assert len(refs) >= 2
|
|
|
|
# Check for memoized reference
|
|
memo_refs = [r for r in refs if r.reference_type == "memoized"]
|
|
assert len(memo_refs) >= 1
|
|
memo_ref = memo_refs[0]
|
|
assert "memoized.ts" in str(memo_ref.file_path)
|
|
assert "memoize(computeExpensive)" in memo_ref.context
|
|
|
|
# Check for direct call
|
|
call_refs = [r for r in refs if r.reference_type == "call"]
|
|
assert len(call_refs) >= 1
|
|
|
|
|
|
class TestSameFileReferences:
|
|
"""Tests for references within the same file."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with same-file reference patterns."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
|
|
# File with internal references
|
|
(src_dir / "recursive.ts").write_text(
|
|
'export function factorial(n: number): number {\n'
|
|
' if (n <= 1) return 1;\n'
|
|
' return n * factorial(n - 1);\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_recursive_references_values(self, project_root):
|
|
"""Test finding recursive calls within same file with exact values."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "recursive.ts"
|
|
|
|
refs = finder.find_references(make_func("factorial", source_file), include_definition=True)
|
|
|
|
# Should find the recursive call
|
|
call_refs = [r for r in refs if r.reference_type == "call"]
|
|
assert len(call_refs) >= 1
|
|
|
|
recursive_ref = call_refs[0]
|
|
assert recursive_ref.line == 3
|
|
assert recursive_ref.caller_function == "factorial"
|
|
assert "factorial(n - 1)" in recursive_ref.context
|
|
|
|
|
|
class TestComplexMultiFileScenarios:
|
|
"""Tests for complex multi-file scenarios inspired by Appsmith."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create a complex multi-file project structure."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
(src_dir / "utils").mkdir()
|
|
(src_dir / "components").mkdir()
|
|
|
|
# Core utility function
|
|
(src_dir / "utils" / "widgetUtils.ts").write_text(
|
|
'export function isLargeWidget(type: string): boolean {\n'
|
|
" return ['TABLE', 'LIST'].includes(type);\n"
|
|
'}\n'
|
|
)
|
|
|
|
# Re-export from index
|
|
(src_dir / "utils" / "index.ts").write_text(
|
|
"export { isLargeWidget } from './widgetUtils';\n"
|
|
)
|
|
|
|
# Component using the function via re-export
|
|
(src_dir / "components" / "Widget.tsx").write_text(
|
|
"import { isLargeWidget } from '../utils';\n"
|
|
'\n'
|
|
'export function Widget({ type }: { type: string }) {\n'
|
|
' const isLarge = isLargeWidget(type);\n'
|
|
' return isLarge;\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_all_references_across_codebase_values(self, project_root):
|
|
"""Test finding all references to isLargeWidget with exact values."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "utils" / "widgetUtils.ts"
|
|
|
|
refs = finder.find_references(make_func("isLargeWidget", source_file))
|
|
|
|
# Should find re-export in index.ts
|
|
reexport_refs = [r for r in refs if r.reference_type == "reexport"]
|
|
assert len(reexport_refs) == 1
|
|
assert "index.ts" in str(reexport_refs[0].file_path)
|
|
|
|
# Should find call in Widget.tsx
|
|
call_refs = [r for r in refs if r.reference_type == "call"]
|
|
assert len(call_refs) >= 1
|
|
widget_ref = next((r for r in call_refs if "Widget.tsx" in str(r.file_path)), None)
|
|
assert widget_ref is not None
|
|
assert widget_ref.line == 4
|
|
assert widget_ref.caller_function == "Widget"
|
|
|
|
def test_format_references_as_markdown_complex(self, project_root):
|
|
"""Test markdown output for complex multi-file scenario."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "utils" / "widgetUtils.ts"
|
|
|
|
refs = finder.find_references(make_func("isLargeWidget", source_file))
|
|
ref_infos = sorted([
|
|
ReferenceInfo(
|
|
file_path=r.file_path, line=r.line, column=r.column,
|
|
end_line=r.end_line, end_column=r.end_column, context=r.context,
|
|
reference_type=r.reference_type, import_name=r.import_name,
|
|
caller_function=r.caller_function,
|
|
)
|
|
for r in refs
|
|
], key=lambda r: str(r.file_path))
|
|
|
|
markdown = _format_references_as_markdown(ref_infos, source_file, project_root, Language.TYPESCRIPT)
|
|
|
|
expected_markdown = (
|
|
'```typescript:src/components/Widget.tsx\n'
|
|
'function Widget({ type }: { type: string }) {\n'
|
|
' const isLarge = isLargeWidget(type);\n'
|
|
' return isLarge;\n'
|
|
'}\n'
|
|
'```\n'
|
|
'```typescript:src/utils/index.ts\n'
|
|
"export { isLargeWidget } from './widgetUtils';\n"
|
|
'```\n'
|
|
)
|
|
assert markdown == expected_markdown
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Tests for edge cases and error handling."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project for edge case testing."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
return tmp_path
|
|
|
|
def test_nonexistent_file(self, project_root):
|
|
"""Test handling of nonexistent source file."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "nonexistent.ts"
|
|
|
|
refs = finder.find_references(make_func("someFunction", source_file))
|
|
|
|
assert refs == []
|
|
|
|
def test_non_exported_function(self, project_root):
|
|
"""Test handling of non-exported function."""
|
|
# Create a file with non-exported function
|
|
(project_root / "src" / "private.ts").write_text(
|
|
'function internalHelper() {\n'
|
|
' return 42;\n'
|
|
'}\n'
|
|
'\n'
|
|
'export function publicFunction() {\n'
|
|
' return internalHelper();\n'
|
|
'}\n'
|
|
)
|
|
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "private.ts"
|
|
|
|
refs = finder.find_references(make_func("internalHelper", source_file))
|
|
|
|
# Should only find internal reference
|
|
assert all(r.file_path == source_file for r in refs)
|
|
|
|
def test_empty_file(self, project_root):
|
|
"""Test handling of empty file."""
|
|
(project_root / "src" / "empty.ts").write_text("")
|
|
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "empty.ts"
|
|
|
|
refs = finder.find_references(make_func("anything", source_file))
|
|
|
|
assert refs == []
|
|
|
|
def test_format_references_empty_list(self, project_root):
|
|
"""Test _format_references_as_markdown with empty list."""
|
|
markdown = _format_references_as_markdown([], project_root / "src" / "file.ts", project_root, Language.TYPESCRIPT)
|
|
assert markdown == ""
|
|
|
|
|
|
class TestCommonJSPatterns:
|
|
"""Tests for CommonJS require/module.exports patterns."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project with CommonJS patterns."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
|
|
# CommonJS module
|
|
(src_dir / "helpers.js").write_text(
|
|
'function processConfig(config) {\n'
|
|
' return { ...config, processed: true };\n'
|
|
'}\n'
|
|
'\n'
|
|
'module.exports = { processConfig };\n'
|
|
)
|
|
|
|
# Consumer using destructured require
|
|
(src_dir / "main.js").write_text(
|
|
"const { processConfig } = require('./helpers');\n"
|
|
'\n'
|
|
'function handleConfig(config) {\n'
|
|
' return processConfig(config);\n'
|
|
'}\n'
|
|
'\n'
|
|
'module.exports = handleConfig;\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_commonjs_references_values(self, project_root):
|
|
"""Test finding CommonJS references with exact values."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "helpers.js"
|
|
|
|
refs = finder.find_references(make_func("processConfig", source_file))
|
|
|
|
assert len(refs) >= 1
|
|
main_ref = next((r for r in refs if "main.js" in str(r.file_path)), None)
|
|
assert main_ref is not None
|
|
assert main_ref.line == 4
|
|
assert main_ref.reference_type == "call"
|
|
assert main_ref.caller_function == "handleConfig"
|
|
|
|
def test_format_references_as_markdown_commonjs(self, project_root):
|
|
"""Test markdown output for CommonJS patterns."""
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "helpers.js"
|
|
|
|
refs = finder.find_references(make_func("processConfig", source_file))
|
|
ref_infos = sorted([
|
|
ReferenceInfo(
|
|
file_path=r.file_path, line=r.line, column=r.column,
|
|
end_line=r.end_line, end_column=r.end_column, context=r.context,
|
|
reference_type=r.reference_type, import_name=r.import_name,
|
|
caller_function=r.caller_function,
|
|
)
|
|
for r in refs
|
|
], key=lambda r: str(r.file_path))
|
|
|
|
markdown = _format_references_as_markdown(ref_infos, source_file, project_root, Language.JAVASCRIPT)
|
|
|
|
expected_markdown = (
|
|
'```javascript:src/main.js\n'
|
|
'function handleConfig(config) {\n'
|
|
' return processConfig(config);\n'
|
|
'}\n'
|
|
'```\n'
|
|
)
|
|
assert markdown == expected_markdown
|
|
|
|
|
|
class TestConvenienceFunction:
|
|
"""Tests for the find_references convenience function."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create a simple project."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
|
|
(src_dir / "utils.ts").write_text(
|
|
'export function helper() {\n'
|
|
' return 42;\n'
|
|
'}\n'
|
|
)
|
|
|
|
(src_dir / "main.ts").write_text(
|
|
"import { helper } from './utils';\n"
|
|
'\n'
|
|
'export function main() {\n'
|
|
' return helper();\n'
|
|
'}\n'
|
|
)
|
|
|
|
return tmp_path
|
|
|
|
def test_find_references_function_values(self, project_root):
|
|
"""Test the find_references convenience function with exact values."""
|
|
source_file = project_root / "src" / "utils.ts"
|
|
|
|
refs = find_references(make_func("helper", source_file), project_root=project_root)
|
|
|
|
assert len(refs) == 1
|
|
ref = refs[0]
|
|
assert "main.ts" in str(ref.file_path)
|
|
assert ref.line == 4
|
|
assert ref.reference_type == "call"
|
|
assert ref.caller_function == "main"
|
|
|
|
|
|
class TestReferenceDataclass:
|
|
"""Tests for Reference dataclass."""
|
|
|
|
def test_reference_creation(self, tmp_path):
|
|
"""Test creating a Reference object."""
|
|
ref = Reference(
|
|
file_path=tmp_path / "test.ts",
|
|
line=10,
|
|
column=5,
|
|
end_line=10,
|
|
end_column=15,
|
|
context="const result = myFunction();",
|
|
reference_type="call",
|
|
import_name="myFunction",
|
|
caller_function="processData",
|
|
)
|
|
|
|
assert ref.line == 10
|
|
assert ref.column == 5
|
|
assert ref.end_line == 10
|
|
assert ref.end_column == 15
|
|
assert ref.reference_type == "call"
|
|
assert ref.import_name == "myFunction"
|
|
assert ref.caller_function == "processData"
|
|
assert ref.context == "const result = myFunction();"
|
|
|
|
def test_reference_without_caller(self, tmp_path):
|
|
"""Test Reference with no caller function."""
|
|
ref = Reference(
|
|
file_path=tmp_path / "test.ts",
|
|
line=1,
|
|
column=0,
|
|
end_line=1,
|
|
end_column=10,
|
|
context="export { fn } from './module';",
|
|
reference_type="reexport",
|
|
import_name="fn",
|
|
)
|
|
|
|
assert ref.caller_function is None
|
|
|
|
|
|
class TestExportedFunctionDataclass:
|
|
"""Tests for ExportedFunction dataclass."""
|
|
|
|
def test_exported_function_named(self, tmp_path):
|
|
"""Test ExportedFunction for named export."""
|
|
exp = ExportedFunction(
|
|
function_name="myHelper",
|
|
export_name="myHelper",
|
|
is_default=False,
|
|
file_path=tmp_path / "utils.ts",
|
|
)
|
|
|
|
assert exp.function_name == "myHelper"
|
|
assert exp.export_name == "myHelper"
|
|
assert exp.is_default is False
|
|
assert exp.file_path == tmp_path / "utils.ts"
|
|
|
|
def test_exported_function_default(self, tmp_path):
|
|
"""Test ExportedFunction for default export."""
|
|
exp = ExportedFunction(
|
|
function_name="processData",
|
|
export_name="default",
|
|
is_default=True,
|
|
file_path=tmp_path / "processor.ts",
|
|
)
|
|
|
|
assert exp.function_name == "processData"
|
|
assert exp.is_default is True
|
|
assert exp.export_name == "default"
|
|
|
|
|
|
class TestReferenceSearchContext:
|
|
"""Tests for ReferenceSearchContext dataclass."""
|
|
|
|
def test_context_defaults(self):
|
|
"""Test default values for ReferenceSearchContext."""
|
|
ctx = ReferenceSearchContext()
|
|
|
|
assert ctx.visited_files == set()
|
|
assert ctx.max_files == 1000
|
|
|
|
def test_context_custom_max_files(self):
|
|
"""Test custom max_files value."""
|
|
ctx = ReferenceSearchContext(max_files=500)
|
|
|
|
assert ctx.max_files == 500
|
|
|
|
|
|
class TestEdgeCasesAdvanced:
|
|
"""Advanced edge case tests to catch potential failures."""
|
|
|
|
@pytest.fixture
|
|
def project_root(self, tmp_path):
|
|
"""Create project for edge case testing."""
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
return tmp_path
|
|
|
|
def test_circular_import_handling(self, project_root):
|
|
"""Test that circular imports don't cause infinite loops."""
|
|
src_dir = project_root / "src"
|
|
|
|
# Create circular import structure
|
|
(src_dir / "a.ts").write_text(
|
|
"import { funcB } from './b';\n"
|
|
'\n'
|
|
'export function funcA() {\n'
|
|
' return funcB() + 1;\n'
|
|
'}\n'
|
|
)
|
|
|
|
(src_dir / "b.ts").write_text(
|
|
"import { funcA } from './a';\n"
|
|
'\n'
|
|
'export function funcB() {\n'
|
|
' return 42;\n'
|
|
'}\n'
|
|
'\n'
|
|
'export function callsA() {\n'
|
|
' return funcA();\n'
|
|
'}\n'
|
|
)
|
|
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "a.ts"
|
|
|
|
# Should not hang or crash
|
|
refs = finder.find_references(make_func("funcA", source_file))
|
|
|
|
# Should find reference in b.ts
|
|
b_refs = [r for r in refs if "b.ts" in str(r.file_path)]
|
|
assert len(b_refs) >= 1
|
|
assert b_refs[0].caller_function == "callsA"
|
|
|
|
def test_syntax_error_graceful_handling(self, project_root):
|
|
"""Test that syntax errors in files are handled gracefully."""
|
|
src_dir = project_root / "src"
|
|
|
|
(src_dir / "valid.ts").write_text(
|
|
'export function validFunction() {\n'
|
|
' return 42;\n'
|
|
'}\n'
|
|
)
|
|
|
|
# Create a file with syntax error
|
|
(src_dir / "invalid.ts").write_text(
|
|
"import { validFunction } from './valid';\n"
|
|
'\n'
|
|
'export function broken( {\n'
|
|
' return validFunction(\n'
|
|
'}\n'
|
|
)
|
|
|
|
finder = ReferenceFinder(project_root)
|
|
source_file = project_root / "src" / "valid.ts"
|
|
|
|
# Should not crash
|
|
refs = finder.find_references(make_func("validFunction", source_file))
|
|
assert isinstance(refs, list)
|