@@ -8189,7 +8189,29 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None:
81898189 # 2. Reference to an unknown placeholder node.
81908190 maybe_type_expr .as_type = None
81918191 return
8192- else : # does not look like an identifier
8192+ elif (leftmost_name := dotted_identifier_leftmost (str_value )) is not None :
8193+ # Dotted-name string (e.g. "builtins.tuple", "typing.Mapping").
8194+ # Look up the leftmost component; if it cannot be a type prefix
8195+ # then the whole dotted name cannot spell a type. Mirrors the
8196+ # IndexExpr-with-MemberExpr-base filter logic below.
8197+ sym = self .lookup (leftmost_name , UnboundType (leftmost_name ), suppress_errors = True )
8198+ if sym is None :
8199+ # Leftmost component does not refer to anything in scope
8200+ maybe_type_expr .as_type = None
8201+ return
8202+ node = sym .node # cache
8203+ if isinstance (node , PlaceholderNode ) and not node .becomes_typeinfo :
8204+ # Either:
8205+ # 1. f'Cannot resolve name "{t.name}" (possible cyclic definition)'
8206+ # 2. Reference to an unknown placeholder node.
8207+ maybe_type_expr .as_type = None
8208+ return
8209+ if isinstance (node , Var ) and not self .var_is_typing_special_form (node ):
8210+ # Leftmost component is a Var: it is a value, so it cannot be
8211+ # the module or class prefix of a dotted type name.
8212+ maybe_type_expr .as_type = None
8213+ return
8214+ else : # does not look like an identifier or dotted identifier
81938215 if '"' in str_value or "'" in str_value :
81948216 # Only valid inside a Literal[...] or Annotated[..., ...] type
81958217 if "[" not in str_value :
@@ -8568,6 +8590,36 @@ def erase_func_annotations(func: FuncDef) -> None:
85688590 func .unanalyzed_type = None
85698591
85708592
8593+ def dotted_identifier_leftmost (s : str ) -> str | None :
8594+ """The leftmost component of s, if s is a dotted identifier, else None.
8595+
8596+ A dotted identifier is two or more identifiers joined by ".", such as
8597+ "builtins.tuple" or "typing.Mapping". A bare identifier is not a dotted
8598+ identifier: callers are expected to handle that case separately.
8599+
8600+ Returns the leftmost component (which is never empty) so that callers
8601+ need not split s a second time to obtain it.
8602+ """
8603+ # NOTE: Scanning with find() rather than s.split(".") avoids allocating a
8604+ # list, since only the leftmost component is ever needed.
8605+ dot = s .find ("." )
8606+ if dot == - 1 :
8607+ return None
8608+ leftmost = s [:dot ]
8609+ if not leftmost .isidentifier ():
8610+ return None
8611+ start = dot + 1
8612+ while True :
8613+ dot = s .find ("." , start )
8614+ if dot == - 1 :
8615+ if not s [start :].isidentifier ():
8616+ return None
8617+ return leftmost
8618+ if not s [start :dot ].isidentifier ():
8619+ return None
8620+ start = dot + 1
8621+
8622+
85718623def has_nontype_char (s : str ) -> bool :
85728624 """Whether s contains a character that cannot appear in a type expression.
85738625
0 commit comments