Skip to content

Commit b77896d

Browse files
fix: handle class methods, aliased imports, and namespace access in JS test discovery
The index-only approach missed tests for class methods (imported by class name), aliased imports (only alias was tracked), and namespace imports (e.g. math.calculate). Adds class_name→methods index, tracks both original+alias names, and extracts namespace member access via regex. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d8e7588 commit b77896d

1 file changed

Lines changed: 30 additions & 13 deletions

File tree

codeflash/languages/javascript/support.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import logging
10+
import re
1011
import subprocess
1112
import xml.etree.ElementTree as ET
1213
from pathlib import Path
@@ -230,11 +231,14 @@ def discover_tests(
230231
"""
231232
result: dict[str, list[TestInfo]] = {}
232233

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

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

252-
# Build a set of imported function names
256+
# Build a set of imported names, resolving aliases and namespace member access
253257
imported_names: set[str] = set()
254258
for imp in imports:
255259
if imp.default_import:
256260
imported_names.add(imp.default_import)
261+
# Extract member access patterns: e.g. `math.calculate(...)` → "calculate"
262+
for m in re.finditer(rf"\b{re.escape(imp.default_import)}\.(\w+)", source):
263+
imported_names.add(m.group(1))
264+
if imp.namespace_import:
265+
imported_names.add(imp.namespace_import)
266+
for m in re.finditer(rf"\b{re.escape(imp.namespace_import)}\.(\w+)", source):
267+
imported_names.add(m.group(1))
257268
for name, alias in imp.named_imports:
258-
imported_names.add(alias or name)
269+
imported_names.add(name)
270+
if alias:
271+
imported_names.add(alias)
259272

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

263-
# Match source functions to tests using the index
264-
# Only check functions that are actually imported in this test file
276+
# Match via indices: function names and class names → qualified names
277+
matched_qualified_names: set[str] = set()
265278
for imported_name in imported_names:
266279
if imported_name in function_name_to_qualified:
267-
qualified_name = function_name_to_qualified[imported_name]
268-
if qualified_name not in result:
269-
result[qualified_name] = []
270-
for test_name in test_functions:
271-
result[qualified_name].append(
272-
TestInfo(test_name=test_name, test_file=test_file, test_class=None)
273-
)
280+
matched_qualified_names.add(function_name_to_qualified[imported_name])
281+
if imported_name in class_name_to_qualified_names:
282+
matched_qualified_names.update(class_name_to_qualified_names[imported_name])
283+
284+
for qualified_name in matched_qualified_names:
285+
if qualified_name not in result:
286+
result[qualified_name] = []
287+
for test_name in test_functions:
288+
result[qualified_name].append(
289+
TestInfo(test_name=test_name, test_file=test_file, test_class=None)
290+
)
274291
except Exception as e:
275292
logger.debug("Failed to analyze test file %s: %s", test_file, e)
276293

0 commit comments

Comments
 (0)