77from __future__ import annotations
88
99import logging
10+ import re
1011import subprocess
1112import xml .etree .ElementTree as ET
1213from 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