mirror of
https://github.com/codeflash-ai/codeflash.git
synced 2026-05-04 18:25:17 +00:00
# Conflicts: # .claude/rules/architecture.md # .claude/rules/code-style.md # .github/workflows/claude.yml # .github/workflows/duplicate-code-detector.yml # codeflash/api/aiservice.py # codeflash/cli_cmds/console.py # codeflash/cli_cmds/logging_config.py # codeflash/code_utils/deduplicate_code.py # codeflash/discovery/discover_unit_tests.py # codeflash/languages/base.py # codeflash/languages/code_replacer.py # codeflash/languages/javascript/mocha_runner.py # codeflash/languages/javascript/support.py # codeflash/languages/python/support.py # codeflash/optimization/function_optimizer.py # codeflash/verification/parse_test_output.py # codeflash/verification/verification_utils.py # codeflash/verification/verifier.py # packages/codeflash/package-lock.json # packages/codeflash/package.json # tests/languages/javascript/test_support_dispatch.py # tests/test_codeflash_capture.py # tests/test_languages/test_javascript_test_runner.py # tests/test_multi_file_code_replacement.py
1107 lines
39 KiB
Python
1107 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
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
|
|
from codeflash.languages.base import Language, ReferenceInfo
|
|
from codeflash.languages.javascript.find_references import (
|
|
ExportedFunction,
|
|
Reference,
|
|
ReferenceFinder,
|
|
ReferenceSearchContext,
|
|
find_references,
|
|
)
|
|
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\nfunction 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\nexport 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\nexport 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\nexport 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)
|