blob: a8380309d3109a5536130ceead43b6269ccb467e [file] [log] [blame]
"""Semantic analysis of NewType definitions.
This is conceptually part of mypy.semanal (semantic analyzer pass 2).
"""
from __future__ import annotations
from mypy import errorcodes as codes
from mypy.errorcodes import ErrorCode
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.messages import MessageBuilder, format_type
from mypy.nodes import (
ARG_POS,
MDEF,
Argument,
AssignmentStmt,
Block,
CallExpr,
Context,
FuncDef,
NameExpr,
NewTypeExpr,
PlaceholderNode,
RefExpr,
StrExpr,
SymbolTableNode,
TypeInfo,
Var,
)
from mypy.options import Options
from mypy.semanal_shared import SemanticAnalyzerInterface, has_placeholder
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type
from mypy.types import (
AnyType,
CallableType,
Instance,
NoneType,
PlaceholderType,
TupleType,
Type,
TypeOfAny,
get_proper_type,
)
class NewTypeAnalyzer:
def __init__(
self, options: Options, api: SemanticAnalyzerInterface, msg: MessageBuilder
) -> None:
self.options = options
self.api = api
self.msg = msg
def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
"""Check if s declares a NewType; if yes, store it in symbol table.
Return True if it's a NewType declaration. The current target may be
deferred as a side effect if the base type is not ready, even if
the return value is True.
The logic in this function mostly copies the logic for visit_class_def()
with a single (non-Generic) base.
"""
var_name, call = self.analyze_newtype_declaration(s)
if var_name is None or call is None:
return False
name = var_name
# OK, now we know this is a NewType. But the base type may be not ready yet,
# add placeholder as we do for ClassDef.
if self.api.is_func_scope():
name += "@" + str(s.line)
fullname = self.api.qualified_name(name)
if not call.analyzed or isinstance(call.analyzed, NewTypeExpr) and not call.analyzed.info:
# Start from labeling this as a future class, as we do for normal ClassDefs.
placeholder = PlaceholderNode(fullname, s, s.line, becomes_typeinfo=True)
self.api.add_symbol(var_name, placeholder, s, can_defer=False)
old_type, should_defer = self.check_newtype_args(var_name, call, s)
old_type = get_proper_type(old_type)
if not isinstance(call.analyzed, NewTypeExpr):
call.analyzed = NewTypeExpr(var_name, old_type, line=call.line, column=call.column)
else:
call.analyzed.old_type = old_type
if old_type is None:
if should_defer:
# Base type is not ready.
self.api.defer()
return True
# Create the corresponding class definition if the aliased type is subtypeable
assert isinstance(call.analyzed, NewTypeExpr)
if isinstance(old_type, TupleType):
newtype_class_info = self.build_newtype_typeinfo(
name, old_type, old_type.partial_fallback, s.line, call.analyzed.info
)
newtype_class_info.update_tuple_type(old_type)
elif isinstance(old_type, Instance):
if old_type.type.is_protocol:
self.fail("NewType cannot be used with protocol classes", s)
newtype_class_info = self.build_newtype_typeinfo(
name, old_type, old_type, s.line, call.analyzed.info
)
else:
if old_type is not None:
message = "Argument 2 to NewType(...) must be subclassable (got {})"
self.fail(
message.format(format_type(old_type, self.options)),
s,
code=codes.VALID_NEWTYPE,
)
# Otherwise the error was already reported.
old_type = AnyType(TypeOfAny.from_error)
object_type = self.api.named_type("builtins.object")
newtype_class_info = self.build_newtype_typeinfo(
name, old_type, object_type, s.line, call.analyzed.info
)
newtype_class_info.fallback_to_any = True
check_for_explicit_any(
old_type, self.options, self.api.is_typeshed_stub_file, self.msg, context=s
)
if self.options.disallow_any_unimported and has_any_from_unimported_type(old_type):
self.msg.unimported_type_becomes_any("Argument 2 to NewType(...)", old_type, s)
# If so, add it to the symbol table.
assert isinstance(call.analyzed, NewTypeExpr)
# As we do for normal classes, create the TypeInfo only once, then just
# update base classes on next iterations (to get rid of placeholders there).
if not call.analyzed.info:
call.analyzed.info = newtype_class_info
else:
call.analyzed.info.bases = newtype_class_info.bases
self.api.add_symbol(var_name, call.analyzed.info, s)
if self.api.is_func_scope():
self.api.add_symbol_skip_local(name, call.analyzed.info)
newtype_class_info.line = s.line
return True
def analyze_newtype_declaration(self, s: AssignmentStmt) -> tuple[str | None, CallExpr | None]:
"""Return the NewType call expression if `s` is a newtype declaration or None otherwise."""
name, call = None, None
if (
len(s.lvalues) == 1
and isinstance(s.lvalues[0], NameExpr)
and isinstance(s.rvalue, CallExpr)
and isinstance(s.rvalue.callee, RefExpr)
and s.rvalue.callee.fullname == "typing.NewType"
):
name = s.lvalues[0].name
if s.type:
self.fail("Cannot declare the type of a NewType declaration", s)
names = self.api.current_symbol_table()
existing = names.get(name)
# Give a better error message than generic "Name already defined".
if (
existing
and not isinstance(existing.node, PlaceholderNode)
and not s.rvalue.analyzed
):
self.fail(f'Cannot redefine "{name}" as a NewType', s)
# This dummy NewTypeExpr marks the call as sufficiently analyzed; it will be
# overwritten later with a fully complete NewTypeExpr if there are no other
# errors with the NewType() call.
call = s.rvalue
return name, call
def check_newtype_args(
self, name: str, call: CallExpr, context: Context
) -> tuple[Type | None, bool]:
"""Ananlyze base type in NewType call.
Return a tuple (type, should defer).
"""
has_failed = False
args, arg_kinds = call.args, call.arg_kinds
if len(args) != 2 or arg_kinds[0] != ARG_POS or arg_kinds[1] != ARG_POS:
self.fail("NewType(...) expects exactly two positional arguments", context)
return None, False
# Check first argument
if not isinstance(args[0], StrExpr):
self.fail("Argument 1 to NewType(...) must be a string literal", context)
has_failed = True
elif args[0].value != name:
msg = 'String argument 1 "{}" to NewType(...) does not match variable name "{}"'
self.fail(msg.format(args[0].value, name), context)
has_failed = True
# Check second argument
msg = "Argument 2 to NewType(...) must be a valid type"
try:
unanalyzed_type = expr_to_unanalyzed_type(args[1], self.options, self.api.is_stub_file)
except TypeTranslationError:
self.fail(msg, context)
return None, False
# We want to use our custom error message (see above), so we suppress
# the default error message for invalid types here.
old_type = get_proper_type(
self.api.anal_type(
unanalyzed_type,
report_invalid_types=False,
allow_placeholder=not self.options.disable_recursive_aliases
and not self.api.is_func_scope(),
)
)
should_defer = False
if isinstance(old_type, PlaceholderType):
old_type = None
if old_type is None:
should_defer = True
# The caller of this function assumes that if we return a Type, it's always
# a valid one. So, we translate AnyTypes created from errors into None.
if isinstance(old_type, AnyType) and old_type.is_from_error:
self.fail(msg, context)
return None, False
return None if has_failed else old_type, should_defer
def build_newtype_typeinfo(
self,
name: str,
old_type: Type,
base_type: Instance,
line: int,
existing_info: TypeInfo | None,
) -> TypeInfo:
info = existing_info or self.api.basic_new_typeinfo(name, base_type, line)
info.bases = [base_type] # Update in case there were nested placeholders.
info.is_newtype = True
# Add __init__ method
args = [
Argument(Var("self"), NoneType(), None, ARG_POS),
self.make_argument("item", old_type),
]
signature = CallableType(
arg_types=[Instance(info, []), old_type],
arg_kinds=[arg.kind for arg in args],
arg_names=["self", "item"],
ret_type=NoneType(),
fallback=self.api.named_type("builtins.function"),
name=name,
)
init_func = FuncDef("__init__", args, Block([]), typ=signature)
init_func.info = info
init_func._fullname = info.fullname + ".__init__"
if not existing_info:
updated = True
else:
previous_sym = info.names["__init__"].node
assert isinstance(previous_sym, FuncDef)
updated = old_type != previous_sym.arguments[1].variable.type
info.names["__init__"] = SymbolTableNode(MDEF, init_func)
if has_placeholder(old_type):
self.api.process_placeholder(None, "NewType base", info, force_progress=updated)
return info
# Helpers
def make_argument(self, name: str, type: Type) -> Argument:
return Argument(Var(name), type, None, ARG_POS)
def fail(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:
self.api.fail(msg, ctx, code=code)