@@ -453,6 +453,14 @@ def __init__(
453
453
# current SCC or top-level function.
454
454
self .deferral_debug_context : List [Tuple [str , int ]] = []
455
455
456
+ # This is needed to properly support recursive type aliases. The problem is that
457
+ # Foo[Bar] could mean three things depending on context: a target for type alias,
458
+ # a normal index expression (including enum index), or a type application.
459
+ # The latter is particularly problematic as it can falsely create incomplete
460
+ # refs while analysing rvalues of type aliases. To avoid this we first analyse
461
+ # rvalues while temporarily setting this to True.
462
+ self .basic_type_applications = False
463
+
456
464
# mypyc doesn't properly handle implementing an abstractproperty
457
465
# with a regular attribute so we make them properties
458
466
@property
@@ -2319,14 +2327,25 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
2319
2327
return
2320
2328
2321
2329
tag = self .track_incomplete_refs ()
2322
- s .rvalue .accept (self )
2330
+
2331
+ # Here we have a chicken and egg problem: at this stage we can't call
2332
+ # can_be_type_alias(), because we have not enough information about rvalue.
2333
+ # But we can't use a full visit because it may emit extra incomplete refs (namely
2334
+ # when analysing any type applications there) thus preventing the further analysis.
2335
+ # To break the tie, we first analyse rvalue partially, if it can be a type alias.
2336
+ with self .basic_type_applications_set (s ):
2337
+ s .rvalue .accept (self )
2323
2338
if self .found_incomplete_ref (tag ) or self .should_wait_rhs (s .rvalue ):
2324
2339
# Initializer couldn't be fully analyzed. Defer the current node and give up.
2325
2340
# Make sure that if we skip the definition of some local names, they can't be
2326
2341
# added later in this scope, since an earlier definition should take precedence.
2327
2342
for expr in names_modified_by_assignment (s ):
2328
2343
self .mark_incomplete (expr .name , expr )
2329
2344
return
2345
+ if self .can_possibly_be_index_alias (s ):
2346
+ # Now re-visit those rvalues that were we skipped type applications above.
2347
+ # This should be safe as generally semantic analyzer is idempotent.
2348
+ s .rvalue .accept (self )
2330
2349
2331
2350
# The r.h.s. is now ready to be classified, first check if it is a special form:
2332
2351
special_form = False
@@ -2465,6 +2484,36 @@ def can_be_type_alias(self, rv: Expression, allow_none: bool = False) -> bool:
2465
2484
return True
2466
2485
return False
2467
2486
2487
+ def can_possibly_be_index_alias (self , s : AssignmentStmt ) -> bool :
2488
+ """Like can_be_type_alias(), but simpler and doesn't require analyzed rvalue.
2489
+
2490
+ Instead, use lvalues/annotations structure to figure out whether this can
2491
+ potentially be a type alias definition. Another difference from above function
2492
+ is that we are only interested IndexExpr and OpExpr rvalues, since only those
2493
+ can be potentially recursive (things like `A = A` are never valid).
2494
+ """
2495
+ if len (s .lvalues ) > 1 :
2496
+ return False
2497
+ if not isinstance (s .lvalues [0 ], NameExpr ):
2498
+ return False
2499
+ if s .unanalyzed_type is not None and not self .is_pep_613 (s ):
2500
+ return False
2501
+ if not isinstance (s .rvalue , (IndexExpr , OpExpr )):
2502
+ return False
2503
+ # Something that looks like Foo = Bar[Baz, ...]
2504
+ return True
2505
+
2506
+ @contextmanager
2507
+ def basic_type_applications_set (self , s : AssignmentStmt ) -> Iterator [None ]:
2508
+ old = self .basic_type_applications
2509
+ # As an optimization, only use the double visit logic if this
2510
+ # can possibly be a recursive type alias.
2511
+ self .basic_type_applications = self .can_possibly_be_index_alias (s )
2512
+ try :
2513
+ yield
2514
+ finally :
2515
+ self .basic_type_applications = old
2516
+
2468
2517
def is_type_ref (self , rv : Expression , bare : bool = False ) -> bool :
2469
2518
"""Does this expression refer to a type?
2470
2519
@@ -2941,6 +2990,13 @@ def analyze_alias(
2941
2990
qualified_tvars = []
2942
2991
return typ , alias_tvars , depends_on , qualified_tvars
2943
2992
2993
+ def is_pep_613 (self , s : AssignmentStmt ) -> bool :
2994
+ if s .unanalyzed_type is not None and isinstance (s .unanalyzed_type , UnboundType ):
2995
+ lookup = self .lookup_qualified (s .unanalyzed_type .name , s , suppress_errors = True )
2996
+ if lookup and lookup .fullname in TYPE_ALIAS_NAMES :
2997
+ return True
2998
+ return False
2999
+
2944
3000
def check_and_set_up_type_alias (self , s : AssignmentStmt ) -> bool :
2945
3001
"""Check if assignment creates a type alias and set it up as needed.
2946
3002
@@ -2955,11 +3011,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
2955
3011
# First rule: Only simple assignments like Alias = ... create aliases.
2956
3012
return False
2957
3013
2958
- pep_613 = False
2959
- if s .unanalyzed_type is not None and isinstance (s .unanalyzed_type , UnboundType ):
2960
- lookup = self .lookup_qualified (s .unanalyzed_type .name , s , suppress_errors = True )
2961
- if lookup and lookup .fullname in TYPE_ALIAS_NAMES :
2962
- pep_613 = True
3014
+ pep_613 = self .is_pep_613 (s )
2963
3015
if not pep_613 and s .unanalyzed_type is not None :
2964
3016
# Second rule: Explicit type (cls: Type[A] = A) always creates variable, not alias.
2965
3017
# unless using PEP 613 `cls: TypeAlias = A`
@@ -3023,9 +3075,16 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
3023
3075
)
3024
3076
if not res :
3025
3077
return False
3026
- # TODO: Maybe we only need to reject top-level placeholders, similar
3027
- # to base classes.
3028
- if self .found_incomplete_ref (tag ) or has_placeholder (res ):
3078
+ if self .options .enable_recursive_aliases :
3079
+ # Only marking incomplete for top-level placeholders makes recursive aliases like
3080
+ # `A = Sequence[str | A]` valid here, similar to how we treat base classes in class
3081
+ # definitions, allowing `class str(Sequence[str]): ...`
3082
+ incomplete_target = isinstance (res , ProperType ) and isinstance (
3083
+ res , PlaceholderType
3084
+ )
3085
+ else :
3086
+ incomplete_target = has_placeholder (res )
3087
+ if self .found_incomplete_ref (tag ) or incomplete_target :
3029
3088
# Since we have got here, we know this must be a type alias (incomplete refs
3030
3089
# may appear in nested positions), therefore use becomes_typeinfo=True.
3031
3090
self .mark_incomplete (lvalue .name , rvalue , becomes_typeinfo = True )
@@ -4532,6 +4591,9 @@ def analyze_type_application_args(self, expr: IndexExpr) -> Optional[List[Type]]
4532
4591
self .analyze_type_expr (index )
4533
4592
if self .found_incomplete_ref (tag ):
4534
4593
return None
4594
+ if self .basic_type_applications :
4595
+ # Postpone the rest until we have more information (for r.h.s. of an assignment)
4596
+ return None
4535
4597
types : List [Type ] = []
4536
4598
if isinstance (index , TupleExpr ):
4537
4599
items = index .items
0 commit comments