11import ast
22import builtins
33import collections .abc
4+ import copy
45import inspect
6+ import keyword
57import linecache
8+ import random
9+ import string
610import sys
711import types
812import typing
@@ -493,6 +497,54 @@ class definitions with proper inheritance, typed attributes, and method stubs.
493497 return nodes
494498
495499
500+ def _generate_unique_name (existing_names : set [str ]) -> str :
501+ """Generate a random valid Python identifier that is not in existing_names.
502+
503+ Produces names like ``_synth_a3f7b2`` that are valid identifiers,
504+ not Python keywords, and not in the given set of existing names.
505+ """
506+ while True :
507+ suffix = "" .join (random .choices (string .ascii_lowercase + string .digits , k = 8 ))
508+ candidate = f"_synth_{ suffix } "
509+ if (
510+ candidate not in existing_names
511+ and candidate .isidentifier ()
512+ and not keyword .iskeyword (candidate )
513+ ):
514+ return candidate
515+
516+
517+ class _RenameTransformer (ast .NodeTransformer ):
518+ """Rename function definitions and their references in a module AST.
519+
520+ Given a mapping ``{old_name: new_name}``, renames:
521+ - ``FunctionDef.name`` for matching definitions
522+ - ``ast.Name.id`` references throughout the entire AST
523+
524+ The rename is applied uniformly because it only targets module-level
525+ function definitions that collide with context variable declarations.
526+ Local assignments inside function bodies are in their own scope and
527+ cannot cause the mypy ``[no-redef]`` error, so they need no special
528+ handling.
529+ """
530+
531+ def __init__ (self , rename_map : dict [str , str ]):
532+ self .rename_map = rename_map
533+
534+ def visit_FunctionDef (self , node : ast .FunctionDef ) -> ast .FunctionDef :
535+ if node .name in self .rename_map :
536+ node .name = self .rename_map [node .name ]
537+ self .generic_visit (node )
538+ return node
539+
540+ visit_AsyncFunctionDef = visit_FunctionDef # type: ignore[assignment]
541+
542+ def visit_Name (self , node : ast .Name ) -> ast .Name :
543+ if node .id in self .rename_map :
544+ node .id = self .rename_map [node .id ]
545+ return node
546+
547+
496548def mypy_type_check (
497549 module : ast .Module ,
498550 ctx : typing .Mapping [str , Any ],
@@ -505,6 +557,9 @@ def mypy_type_check(
505557 appends the module body, then a postlude that assigns the last function to a
506558 variable annotated with Callable[expected_params, expected_return]. Runs mypy
507559 on the combined source; raises TypeError with the mypy report on failure.
560+
561+ If the synthesized function name clashes with a name already in the context,
562+ the function is renamed to a unique random identifier for type-checking only.
508563 """
509564 if not module .body :
510565 raise TypeError ("mypy_type_check: module.body is empty" )
@@ -527,6 +582,43 @@ def mypy_type_check(
527582 stubs = collect_runtime_type_stubs (ctx )
528583 variables = collect_variable_declarations (ctx )
529584
585+ # Collect names already declared in the type-checking preamble
586+ # (variable declarations and class stubs) that could collide with
587+ # function definitions in the synthesized module.
588+ declared_names = {
589+ stmt .target .id
590+ for stmt in variables
591+ if isinstance (stmt , ast .AnnAssign ) and isinstance (stmt .target , ast .Name )
592+ } | {stmt .name for stmt in stubs if isinstance (stmt , ast .ClassDef )}
593+
594+ # Find all function names in the synthesized module that collide
595+ synthesized_func_names = {
596+ stmt .name
597+ for stmt in module .body
598+ if isinstance (stmt , (ast .FunctionDef , ast .AsyncFunctionDef ))
599+ }
600+ colliding_names = synthesized_func_names & declared_names
601+
602+ if colliding_names :
603+ # Build a rename map for every colliding function name
604+ all_reserved = declared_names | synthesized_func_names
605+ rename_map : dict [str , str ] = {}
606+ for name in colliding_names :
607+ unique = _generate_unique_name (all_reserved )
608+ rename_map [name ] = unique
609+ all_reserved .add (unique )
610+
611+ # Deep-copy the module body so we don't mutate the caller's AST,
612+ # then rename definitions and all references to them.
613+ module_body = copy .deepcopy (list (module .body ))
614+ stub_module_body = ast .Module (body = module_body , type_ignores = [])
615+ _RenameTransformer (rename_map ).visit (stub_module_body )
616+ module_body = stub_module_body .body
617+ tc_func_name = rename_map .get (func_name , func_name )
618+ else :
619+ module_body = list (module .body )
620+ tc_func_name = func_name
621+
530622 param_types = expected_params
531623 expected_callable_type : type = typing .cast (
532624 type ,
@@ -539,15 +631,15 @@ def mypy_type_check(
539631 postlude = ast .AnnAssign (
540632 target = ast .Name (id = "_synthesized_check" , ctx = ast .Store ()),
541633 annotation = expected_callable_ast ,
542- value = ast .Name (id = func_name , ctx = ast .Load ()),
634+ value = ast .Name (id = tc_func_name , ctx = ast .Load ()),
543635 simple = 1 ,
544636 )
545637 full_body = (
546638 baseline_imports
547639 + list (imports )
548640 + list (stubs )
549641 + list (variables )
550- + list ( module . body )
642+ + module_body
551643 + [postlude ]
552644 )
553645 stub_module = ast .Module (body = full_body , type_ignores = [])
0 commit comments