@@ -151,6 +151,15 @@ def __init__(self, function_names_to_find: set[str]) -> None:
151151 self .has_dynamic_imports : bool = False
152152 self .wildcard_modules : set [str ] = set ()
153153
154+ # Precompute function_names for prefix search
155+ # For prefix match, store mapping from prefix-root to candidates for O(1) matching
156+ self ._exact_names = function_names_to_find
157+ self ._prefix_roots = {}
158+ for name in function_names_to_find :
159+ if "." in name :
160+ root = name .split ("." , 1 )[0 ]
161+ self ._prefix_roots .setdefault (root , []).append (name )
162+
154163 def visit_Import (self , node : ast .Import ) -> None :
155164 """Handle 'import module' statements."""
156165 if self .found_any_target_function :
@@ -181,40 +190,48 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
181190 if self .found_any_target_function :
182191 return
183192
184- if not node .module :
193+ mod = node .module
194+ if not mod :
185195 return
186196
197+ fnames = self ._exact_names
198+ proots = self ._prefix_roots
199+
187200 for alias in node .names :
188- if alias .name == "*" :
189- self .wildcard_modules .add (node .module )
190- else :
191- imported_name = alias .asname if alias .asname else alias .name
192- self .imported_modules .add (imported_name )
201+ aname = alias .name
202+ if aname == "*" :
203+ self .wildcard_modules .add (mod )
204+ continue
193205
194- # Check for dynamic import functions
195- if node .module == "importlib" and alias .name == "import_module" :
196- self .has_dynamic_imports = True
206+ imported_name = alias .asname if alias .asname else aname
207+ self .imported_modules .add (imported_name )
208+
209+ # Fast check for dynamic import
210+ if mod == "importlib" and aname == "import_module" :
211+ self .has_dynamic_imports = True
197212
198- qualified_name = f"{ node .module } .{ alias .name } "
199- potential_matches = {alias .name , qualified_name }
213+ qname = f"{ mod } .{ aname } "
200214
201- if any (name in self .function_names_to_find for name in potential_matches ):
215+ # Fast exact match check
216+ if aname in fnames :
202217 self .found_any_target_function = True
203- self .found_qualified_name = next (
204- name for name in potential_matches if name in self .function_names_to_find
205- )
218+ self .found_qualified_name = aname
206219 return
207-
208- qualified_prefix = qualified_name + "."
209- if any (target_func .startswith (qualified_prefix ) for target_func in self .function_names_to_find ):
220+ if qname in fnames :
210221 self .found_any_target_function = True
211- self .found_qualified_name = next (
212- target_func
213- for target_func in self .function_names_to_find
214- if target_func .startswith (qualified_prefix )
215- )
222+ self .found_qualified_name = qname
216223 return
217224
225+ # Fast prefix match: only for relevant roots
226+ prefix = qname + "."
227+ # Only bother if one of the targets startswith the prefix-root
228+ candidates = proots .get (qname , ())
229+ for target_func in candidates :
230+ if target_func .startswith (prefix ):
231+ self .found_any_target_function = True
232+ self .found_qualified_name = target_func
233+ return
234+
218235 def visit_Attribute (self , node : ast .Attribute ) -> None :
219236 """Handle attribute access like module.function_name."""
220237 if self .found_any_target_function :
0 commit comments