Skip to content

Commit b2b43fe

Browse files
davidfstrclaude
andcommitted
TypeForm: Dotted-identifier-strings: Early-reject non-type leftmost component in try_parse_as_type_expression()
Implemented with plain string operations rather than a regular expression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 159885c commit b2b43fe

1 file changed

Lines changed: 53 additions & 1 deletion

File tree

mypy/semanal.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
85718623
def has_nontype_char(s: str) -> bool:
85728624
"""Whether s contains a character that cannot appear in a type expression.
85738625

0 commit comments

Comments
 (0)