Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions codeflash/languages/javascript/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from __future__ import annotations

import logging
import re
import subprocess
import xml.etree.ElementTree as ET
from pathlib import Path
Expand Down Expand Up @@ -230,11 +231,14 @@ def discover_tests(
"""
result: dict[str, list[TestInfo]] = {}

# Build index: function_name → qualified_name for O(1) lookup
# This avoids iterating all functions for every test file (was O(NxM), now O(N+M))
# Build indices for O(1) lookup per imported name (avoids O(NxM) loop)
function_name_to_qualified: dict[str, str] = {}
class_name_to_qualified_names: dict[str, list[str]] = {}
for func in source_functions:
function_name_to_qualified[func.function_name] = func.qualified_name
for parent in func.parents:
if parent.type == "ClassDef":
class_name_to_qualified_names.setdefault(parent.name, []).append(func.qualified_name)

# Find all test files using language-specific patterns
test_patterns = self._get_test_patterns()
Expand All @@ -249,28 +253,41 @@ def discover_tests(
analyzer = get_analyzer_for_file(test_file)
imports = analyzer.find_imports(source)

# Build a set of imported function names
# Build a set of imported names, resolving aliases and namespace member access
imported_names: set[str] = set()
for imp in imports:
if imp.default_import:
imported_names.add(imp.default_import)
# Extract member access patterns: e.g. `math.calculate(...)` → "calculate"
for m in re.finditer(rf"\b{re.escape(imp.default_import)}\.(\w+)", source):
imported_names.add(m.group(1))
if imp.namespace_import:
imported_names.add(imp.namespace_import)
for m in re.finditer(rf"\b{re.escape(imp.namespace_import)}\.(\w+)", source):
imported_names.add(m.group(1))
for name, alias in imp.named_imports:
imported_names.add(alias or name)
imported_names.add(name)
if alias:
imported_names.add(alias)

# Find test functions (describe/it/test blocks)
test_functions = self._find_jest_tests(source, analyzer)

# Match source functions to tests using the index
# Only check functions that are actually imported in this test file
# Match via indices: function names and class names → qualified names
matched_qualified_names: set[str] = set()
for imported_name in imported_names:
if imported_name in function_name_to_qualified:
qualified_name = function_name_to_qualified[imported_name]
if qualified_name not in result:
result[qualified_name] = []
for test_name in test_functions:
result[qualified_name].append(
TestInfo(test_name=test_name, test_file=test_file, test_class=None)
)
matched_qualified_names.add(function_name_to_qualified[imported_name])
if imported_name in class_name_to_qualified_names:
matched_qualified_names.update(class_name_to_qualified_names[imported_name])

for qualified_name in matched_qualified_names:
if qualified_name not in result:
result[qualified_name] = []
for test_name in test_functions:
result[qualified_name].append(
TestInfo(test_name=test_name, test_file=test_file, test_class=None)
)
except Exception as e:
logger.debug("Failed to analyze test file %s: %s", test_file, e)

Expand Down
109 changes: 109 additions & 0 deletions tests/languages/javascript/test_false_positive_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Test for false positive test discovery bug (Bug #4)."""

from pathlib import Path
from tempfile import TemporaryDirectory

import pytest

from codeflash.discovery.functions_to_optimize import FunctionToOptimize
from codeflash.languages.javascript.support import TypeScriptSupport
from codeflash.models.models import CodePosition


def test_discover_tests_should_not_match_mocked_functions():
"""Test that functions mentioned only in mocks are not matched as test targets.

Regression test for Bug #4: False positive test discovery due to substring matching.

When a test file mocks a function (e.g., vi.mock("./restart-request.js", () => ({...}))),
that function should NOT be considered as tested by that file, since it's only mocked,
not actually called or tested.
"""
support = TypeScriptSupport()

with TemporaryDirectory() as tmpdir:
test_root = Path(tmpdir)

# Create a test file that MOCKS parseRestartRequestParams but doesn't test it
test_file = test_root / "update.test.ts"
test_file.write_text(
'''
import { updateSomething } from "./update.js";

vi.mock("./restart-request.js", () => ({
parseRestartRequestParams: (params: any) => ({ sessionKey: undefined }),
}));

describe("updateSomething", () => {
it("should update successfully", () => {
const result = updateSomething();
expect(result).toBe(true);
});
});
'''
)

# Source function that is only mocked, not tested
source_function = FunctionToOptimize(
qualified_name="parseRestartRequestParams",
function_name="parseRestartRequestParams",
file_path=test_root / "restart-request.ts",
starting_line=1,
ending_line=10,
function_signature="",
code_position=CodePosition(line_no=1, col_no=0),
file_path_relative_to_project_root="restart-request.ts",
)

# Discover tests
result = support.discover_tests(test_root, [source_function])

# The bug: discovers update.test.ts as a test for parseRestartRequestParams
# because "parseRestartRequestParams" appears as a substring in the mock
# Expected: should NOT match (empty result)
assert (
source_function.qualified_name not in result or len(result[source_function.qualified_name]) == 0
), f"Should not match mocked function, but found: {result.get(source_function.qualified_name, [])}"


def test_discover_tests_should_match_actually_imported_functions():
"""Test that functions actually imported and tested ARE correctly matched.

This is the positive case to ensure we don't break legitimate test discovery.
"""
support = TypeScriptSupport()

with TemporaryDirectory() as tmpdir:
test_root = Path(tmpdir)

# Create a test file that ACTUALLY imports and tests the function
test_file = test_root / "restart-request.test.ts"
test_file.write_text(
'''
import { parseRestartRequestParams } from "./restart-request.js";

describe("parseRestartRequestParams", () => {
it("should parse valid params", () => {
const result = parseRestartRequestParams({ sessionKey: "abc" });
expect(result.sessionKey).toBe("abc");
});
});
'''
)

source_function = FunctionToOptimize(
qualified_name="parseRestartRequestParams",
function_name="parseRestartRequestParams",
file_path=test_root / "restart-request.ts",
starting_line=1,
ending_line=10,
function_signature="",
code_position=CodePosition(line_no=1, col_no=0),
file_path_relative_to_project_root="restart-request.ts",
)

result = support.discover_tests(test_root, [source_function])

# Should match: function is imported and tested
assert source_function.qualified_name in result, f"Should match imported function, but got: {result}"
assert len(result[source_function.qualified_name]) > 0, "Should find at least one test"
Loading