@@ -551,53 +551,53 @@ def _analyze_imports_in_optimized_code(
551551 """
552552 imported_names_map = defaultdict (set )
553553
554- # Precompute a two-level dict: module_name -> func_name -> [helpers]
555- helpers_by_file_and_func = defaultdict (dict )
556- helpers_by_file = defaultdict (list ) # preserved for "import module"
557- helpers_append = helpers_by_file_and_func .setdefault
554+ # Prepare one-pass lookup: module_name -> func_name -> [helpers], and module_name -> [helpers]
555+ helpers_by_file_and_func = {}
556+ helpers_by_file = {}
558557 for helper in code_context .helper_functions :
559558 jedi_type = helper .jedi_definition .type
560- if jedi_type != "class" :
561- func_name = helper .only_function_name
562- module_name = helper .file_path .stem
563- # Cache function lookup for this (module, func)
564- file_entry = helpers_by_file_and_func [module_name ]
565- if func_name in file_entry :
566- file_entry [func_name ].append (helper )
567- else :
568- file_entry [func_name ] = [helper ]
569- helpers_by_file [module_name ].append (helper )
570-
571- # Optimize attribute lookups and method binding outside the loop
559+ if jedi_type == "class" :
560+ continue
561+ func_name = helper .only_function_name
562+ module_name = helper .file_path .stem
563+ file_entry = helpers_by_file_and_func .setdefault (module_name , {})
564+ file_entry .setdefault (func_name , []).append (helper )
565+ helpers_by_file .setdefault (module_name , []).append (helper )
566+
567+ # Optimize lookups: create shortcut functions
572568 helpers_by_file_and_func_get = helpers_by_file_and_func .get
573569 helpers_by_file_get = helpers_by_file .get
574570
575- for node in ast .walk (optimized_ast ):
571+ # Only walk once for imports, use a generator for both Import and ImportFrom
572+ nodes = [n for n in ast .iter_child_nodes (optimized_ast ) if isinstance (n , (ast .Import , ast .ImportFrom ))]
573+ for node in nodes :
576574 if isinstance (node , ast .ImportFrom ):
577- # Handle "from module import function" statements
578575 module_name = node .module
579576 if module_name :
580- file_entry = helpers_by_file_and_func_get (module_name , None )
577+ file_entry = helpers_by_file_and_func_get (module_name )
581578 if file_entry :
582579 for alias in node .names :
583- imported_name = alias .asname if alias . asname else alias .name
580+ imported_name = alias .asname or alias .name
584581 original_name = alias .name
585- helpers = file_entry .get (original_name , None )
582+ helpers = file_entry .get (original_name )
586583 if helpers :
584+ # Only add each possible helper name once
585+ imported_set = imported_names_map [imported_name ]
587586 for helper in helpers :
588- imported_names_map [imported_name ].add (helper .qualified_name )
589- imported_names_map [imported_name ].add (helper .fully_qualified_name )
590-
587+ imported_set .add (helper .qualified_name )
588+ imported_set .add (helper .fully_qualified_name )
591589 elif isinstance (node , ast .Import ):
592- # Handle "import module" statements
593590 for alias in node .names :
594- imported_name = alias .asname if alias . asname else alias .name
591+ imported_name = alias .asname or alias .name
595592 module_name = alias .name
596- for helper in helpers_by_file_get (module_name , []):
597- # For "import module" statements, functions would be called as module.function
598- full_call = f"{ imported_name } .{ helper .only_function_name } "
599- imported_names_map [full_call ].add (helper .qualified_name )
600- imported_names_map [full_call ].add (helper .fully_qualified_name )
593+ helpers_list = helpers_by_file_get (module_name )
594+ if helpers_list :
595+ for helper in helpers_list :
596+ # "import module": functions called as module.function
597+ full_call = f"{ imported_name } .{ helper .only_function_name } "
598+ callset = imported_names_map [full_call ]
599+ callset .add (helper .qualified_name )
600+ callset .add (helper .fully_qualified_name )
601601
602602 return dict (imported_names_map )
603603
@@ -616,88 +616,88 @@ def detect_unused_helper_functions(
616616
617617 """
618618 try :
619- # Parse the optimized code to analyze function calls and imports
620619 optimized_ast = ast .parse (optimized_code )
621620
622- # Find the optimized entrypoint function
623- entrypoint_function_ast = None
624- for node in ast .walk (optimized_ast ):
625- if isinstance (node , ast .FunctionDef ) and node .name == function_to_optimize .function_name :
626- entrypoint_function_ast = node
627- break
621+ # Find the optimized entrypoint function early (using generator for early break)
622+ entrypoint_function_name = function_to_optimize .function_name
623+ entrypoint_function_ast = next (
624+ (
625+ node
626+ for node in ast .walk (optimized_ast )
627+ if isinstance (node , ast .FunctionDef ) and node .name == entrypoint_function_name
628+ ),
629+ None ,
630+ )
628631
629632 if not entrypoint_function_ast :
630- logger .debug (f"Could not find entrypoint function { function_to_optimize . function_name } in optimized code" )
633+ logger .debug (f"Could not find entrypoint function { entrypoint_function_name } in optimized code" )
631634 return []
632635
633- # First, analyze imports to build a mapping of imported names to their original qualified names
634636 imported_names_map = _analyze_imports_in_optimized_code (optimized_ast , code_context )
635637
636- # Extract all function calls in the entrypoint function
638+ # Extract all called function names in entrypoint AST, collecting variants in one pass
637639 called_function_names = set ()
640+ parents = getattr (function_to_optimize , "parents" , None )
641+ class_name = parents [0 ].name if parents else None
642+
638643 for node in ast .walk (entrypoint_function_ast ):
639644 if isinstance (node , ast .Call ):
640- if isinstance ( node .func , ast . Name ):
641- # Regular function call: function_name()
642- called_name = node . func .id
645+ func = node .func
646+ if isinstance ( func , ast . Name ):
647+ called_name = func .id
643648 called_function_names .add (called_name )
644- # Also add the qualified name if this is an imported function
645649 if called_name in imported_names_map :
646650 called_function_names .update (imported_names_map [called_name ])
647- elif isinstance (node . func , ast .Attribute ):
648- # Method call: obj.method() or self.method() or module.function()
649- if isinstance ( node . func .value , ast . Name ):
650- if node . func . value . id == "self" :
651- # self.method_name() -> add both method_name and ClassName.method_name
652- called_function_names . add ( node . func . attr )
653- # For class methods, also add the qualified name
654- if hasattr ( function_to_optimize , "parents" ) and function_to_optimize . parents :
655- class_name = function_to_optimize . parents [ 0 ]. name
656- called_function_names .add (f"{ class_name } .{ node . func . attr } " )
651+ elif isinstance (func , ast .Attribute ):
652+ val = func . value
653+ attr_name = func .attr
654+ # Method call: self.method() or module.function() or obj.method()
655+ if isinstance ( val , ast . Name ):
656+ val_id = val . id
657+ if val_id == "self" :
658+ called_function_names . add ( attr_name )
659+ if class_name :
660+ called_function_names .add (f"{ class_name } .{ attr_name } " )
657661 else :
658- # obj.method() or module.function()
659- attr_name = node .func .attr
660662 called_function_names .add (attr_name )
661- called_function_names .add (f"{ node .func .value .id } .{ attr_name } " )
662- # Check if this is a module.function call that maps to a helper
663- full_call = f"{ node .func .value .id } .{ attr_name } "
663+ full_call = f"{ val_id } .{ attr_name } "
664+ called_function_names .add (full_call )
664665 if full_call in imported_names_map :
665666 called_function_names .update (imported_names_map [full_call ])
666- # Handle nested attribute access like obj.attr.method()
667667 else :
668- called_function_names .add (node .func .attr )
668+ # obj.attr.method()
669+ called_function_names .add (attr_name )
669670
670671 logger .debug (f"Functions called in optimized entrypoint: { called_function_names } " )
671672 logger .debug (f"Imported names mapping: { imported_names_map } " )
672673
673- # Find helper functions that are no longer called
674+ # Precompute entrypoint's file_path for fast comparison
675+ entrypoint_file_path = function_to_optimize .file_path
676+
677+ # Prefetch attributes to reduce lookup cost inside loop
674678 unused_helpers = []
675679 for helper_function in code_context .helper_functions :
676- if helper_function .jedi_definition .type != "class" :
677- # Check if the helper function is called using multiple name variants
678- helper_qualified_name = helper_function .qualified_name
679- helper_simple_name = helper_function .only_function_name
680- helper_fully_qualified_name = helper_function .fully_qualified_name
681-
682- # Create a set of all possible names this helper might be called by
683- possible_call_names = {helper_qualified_name , helper_simple_name , helper_fully_qualified_name }
684-
685- # For cross-file helpers, also consider module-based calls
686- if helper_function .file_path != function_to_optimize .file_path :
687- # Add potential module.function combinations
688- module_name = helper_function .file_path .stem
689- possible_call_names .add (f"{ module_name } .{ helper_simple_name } " )
690-
691- # Check if any of the possible names are in the called functions
692- is_called = bool (possible_call_names .intersection (called_function_names ))
693-
694- if not is_called :
695- unused_helpers .append (helper_function )
696- logger .debug (f"Helper function { helper_qualified_name } is not called in optimized code" )
697- logger .debug (f" Checked names: { possible_call_names } " )
698- else :
699- logger .debug (f"Helper function { helper_qualified_name } is still called in optimized code" )
700- logger .debug (f" Called via: { possible_call_names .intersection (called_function_names )} " )
680+ jedi_type = helper_function .jedi_definition .type
681+ if jedi_type == "class" :
682+ continue
683+
684+ helper_qualified_name = helper_function .qualified_name
685+ helper_simple_name = helper_function .only_function_name
686+ helper_fully_qualified_name = helper_function .fully_qualified_name
687+ possible_call_names = {helper_qualified_name , helper_simple_name , helper_fully_qualified_name }
688+
689+ # For cross-file helpers, add module.function variant
690+ if helper_function .file_path != entrypoint_file_path :
691+ module_name = helper_function .file_path .stem
692+ possible_call_names .add (f"{ module_name } .{ helper_simple_name } " )
693+
694+ if not possible_call_names & called_function_names :
695+ unused_helpers .append (helper_function )
696+ logger .debug (f"Helper function { helper_qualified_name } is not called in optimized code" )
697+ logger .debug (f" Checked names: { possible_call_names } " )
698+ else :
699+ logger .debug (f"Helper function { helper_qualified_name } is still called in optimized code" )
700+ logger .debug (f" Called via: { possible_call_names & called_function_names } " )
701701
702702 return unused_helpers
703703
0 commit comments