| """The semantic analyzer. |
| |
| Bind names to definitions and do various other simple consistency |
| checks. Populate symbol tables. The semantic analyzer also detects |
| special forms which reuse generic syntax such as NamedTuple and |
| cast(). Multiple analysis iterations may be needed to analyze forward |
| references and import cycles. Each iteration "fills in" additional |
| bindings and references until everything has been bound. |
| |
| For example, consider this program: |
| |
| x = 1 |
| y = x |
| |
| Here semantic analysis would detect that the assignment 'x = 1' |
| defines a new variable, the type of which is to be inferred (in a |
| later pass; type inference or type checking is not part of semantic |
| analysis). Also, it would bind both references to 'x' to the same |
| module-level variable (Var) node. The second assignment would also |
| be analyzed, and the type of 'y' marked as being inferred. |
| |
| Semantic analysis of types is implemented in typeanal.py. |
| |
| See semanal_main.py for the top-level logic. |
| |
| Some important properties: |
| |
| * After semantic analysis is complete, no PlaceholderNode and |
| PlaceholderType instances should remain. During semantic analysis, |
| if we encounter one of these, the current target should be deferred. |
| |
| * A TypeInfo is only created once we know certain basic information about |
| a type, such as the MRO, existence of a Tuple base class (e.g., for named |
| tuples), and whether we have a TypedDict. We use a temporary |
| PlaceholderNode node in the symbol table if some such information is |
| missing. |
| |
| * For assignments, we only add a non-placeholder symbol table entry once |
| we know the sort of thing being defined (variable, NamedTuple, type alias, |
| etc.). |
| |
| * Every part of the analysis step must support multiple iterations over |
| the same AST nodes, and each iteration must be able to fill in arbitrary |
| things that were missing or incomplete in previous iterations. |
| |
| * Changes performed by the analysis need to be reversible, since mypy |
| daemon strips and reuses existing ASTs (to improve performance and/or |
| reduce memory use). |
| """ |
| |
| from __future__ import annotations |
| |
| from contextlib import contextmanager |
| from typing import Any, Callable, Collection, Final, Iterable, Iterator, List, TypeVar, cast |
| from typing_extensions import TypeAlias as _TypeAlias |
| |
| from mypy import errorcodes as codes, message_registry |
| from mypy.constant_fold import constant_fold_expr |
| from mypy.errorcodes import ErrorCode |
| from mypy.errors import Errors, report_internal_error |
| from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type |
| from mypy.messages import ( |
| SUGGESTED_TEST_FIXTURES, |
| TYPES_FOR_UNIMPORTED_HINTS, |
| MessageBuilder, |
| best_matches, |
| pretty_seq, |
| ) |
| from mypy.mro import MroError, calculate_mro |
| from mypy.nodes import ( |
| ARG_NAMED, |
| ARG_POS, |
| ARG_STAR, |
| ARG_STAR2, |
| CONTRAVARIANT, |
| COVARIANT, |
| GDEF, |
| IMPLICITLY_ABSTRACT, |
| INVARIANT, |
| IS_ABSTRACT, |
| LDEF, |
| MDEF, |
| NOT_ABSTRACT, |
| REVEAL_LOCALS, |
| REVEAL_TYPE, |
| RUNTIME_PROTOCOL_DECOS, |
| ArgKind, |
| AssertStmt, |
| AssertTypeExpr, |
| AssignmentExpr, |
| AssignmentStmt, |
| AwaitExpr, |
| Block, |
| BreakStmt, |
| CallExpr, |
| CastExpr, |
| ClassDef, |
| ComparisonExpr, |
| ConditionalExpr, |
| Context, |
| ContinueStmt, |
| DataclassTransformSpec, |
| Decorator, |
| DelStmt, |
| DictExpr, |
| DictionaryComprehension, |
| EllipsisExpr, |
| EnumCallExpr, |
| Expression, |
| ExpressionStmt, |
| FakeExpression, |
| ForStmt, |
| FuncBase, |
| FuncDef, |
| FuncItem, |
| GeneratorExpr, |
| GlobalDecl, |
| IfStmt, |
| Import, |
| ImportAll, |
| ImportBase, |
| ImportFrom, |
| IndexExpr, |
| LambdaExpr, |
| ListComprehension, |
| ListExpr, |
| Lvalue, |
| MatchStmt, |
| MemberExpr, |
| MypyFile, |
| NamedTupleExpr, |
| NameExpr, |
| Node, |
| NonlocalDecl, |
| OperatorAssignmentStmt, |
| OpExpr, |
| OverloadedFuncDef, |
| OverloadPart, |
| ParamSpecExpr, |
| PassStmt, |
| PlaceholderNode, |
| PromoteExpr, |
| RaiseStmt, |
| RefExpr, |
| ReturnStmt, |
| RevealExpr, |
| SetComprehension, |
| SetExpr, |
| SliceExpr, |
| StarExpr, |
| Statement, |
| StrExpr, |
| SuperExpr, |
| SymbolNode, |
| SymbolTable, |
| SymbolTableNode, |
| TempNode, |
| TryStmt, |
| TupleExpr, |
| TypeAlias, |
| TypeAliasExpr, |
| TypeApplication, |
| TypedDictExpr, |
| TypeInfo, |
| TypeVarExpr, |
| TypeVarLikeExpr, |
| TypeVarTupleExpr, |
| UnaryExpr, |
| Var, |
| WhileStmt, |
| WithStmt, |
| YieldExpr, |
| YieldFromExpr, |
| get_member_expr_fullname, |
| get_nongen_builtins, |
| implicit_module_attrs, |
| is_final_node, |
| type_aliases, |
| type_aliases_source_versions, |
| typing_extensions_aliases, |
| ) |
| from mypy.options import TYPE_VAR_TUPLE, Options |
| from mypy.patterns import ( |
| AsPattern, |
| ClassPattern, |
| MappingPattern, |
| OrPattern, |
| SequencePattern, |
| StarredPattern, |
| ValuePattern, |
| ) |
| from mypy.plugin import ( |
| ClassDefContext, |
| DynamicClassDefContext, |
| Plugin, |
| SemanticAnalyzerPluginInterface, |
| ) |
| from mypy.plugins import dataclasses as dataclasses_plugin |
| from mypy.reachability import ( |
| ALWAYS_FALSE, |
| ALWAYS_TRUE, |
| MYPY_FALSE, |
| MYPY_TRUE, |
| infer_condition_value, |
| infer_reachability_of_if_statement, |
| infer_reachability_of_match_statement, |
| ) |
| from mypy.scope import Scope |
| from mypy.semanal_enum import EnumCallAnalyzer |
| from mypy.semanal_namedtuple import NamedTupleAnalyzer |
| from mypy.semanal_newtype import NewTypeAnalyzer |
| from mypy.semanal_shared import ( |
| ALLOW_INCOMPATIBLE_OVERRIDE, |
| PRIORITY_FALLBACKS, |
| SemanticAnalyzerInterface, |
| calculate_tuple_fallback, |
| find_dataclass_transform_spec, |
| has_placeholder, |
| parse_bool, |
| require_bool_literal_argument, |
| set_callable_name as set_callable_name, |
| ) |
| from mypy.semanal_typeddict import TypedDictAnalyzer |
| from mypy.tvar_scope import TypeVarLikeScope |
| from mypy.typeanal import ( |
| SELF_TYPE_NAMES, |
| TypeAnalyser, |
| TypeVarLikeList, |
| TypeVarLikeQuery, |
| analyze_type_alias, |
| check_for_explicit_any, |
| detect_diverging_alias, |
| find_self_type, |
| fix_instance_types, |
| has_any_from_unimported_type, |
| no_subscript_builtin_alias, |
| type_constructors, |
| ) |
| from mypy.typeops import function_type, get_type_vars, try_getting_str_literals_from_type |
| from mypy.types import ( |
| ASSERT_TYPE_NAMES, |
| DATACLASS_TRANSFORM_NAMES, |
| FINAL_DECORATOR_NAMES, |
| FINAL_TYPE_NAMES, |
| NEVER_NAMES, |
| OVERLOAD_NAMES, |
| OVERRIDE_DECORATOR_NAMES, |
| PROTOCOL_NAMES, |
| REVEAL_TYPE_NAMES, |
| TPDICT_NAMES, |
| TYPE_ALIAS_NAMES, |
| TYPED_NAMEDTUPLE_NAMES, |
| AnyType, |
| CallableType, |
| FunctionLike, |
| Instance, |
| LiteralType, |
| NoneType, |
| Overloaded, |
| Parameters, |
| ParamSpecType, |
| PlaceholderType, |
| ProperType, |
| TrivialSyntheticTypeTranslator, |
| TupleType, |
| Type, |
| TypeAliasType, |
| TypedDictType, |
| TypeOfAny, |
| TypeType, |
| TypeVarLikeType, |
| TypeVarTupleType, |
| TypeVarType, |
| UnboundType, |
| UnpackType, |
| get_proper_type, |
| get_proper_types, |
| is_named_instance, |
| remove_dups, |
| ) |
| from mypy.types_utils import is_invalid_recursive_alias, store_argument_type |
| from mypy.typevars import fill_typevars |
| from mypy.util import ( |
| correct_relative_import, |
| is_dunder, |
| is_typeshed_file, |
| module_prefix, |
| unmangle, |
| unnamed_function, |
| ) |
| from mypy.visitor import NodeVisitor |
| |
| T = TypeVar("T") |
| |
| |
| FUTURE_IMPORTS: Final = { |
| "__future__.nested_scopes": "nested_scopes", |
| "__future__.generators": "generators", |
| "__future__.division": "division", |
| "__future__.absolute_import": "absolute_import", |
| "__future__.with_statement": "with_statement", |
| "__future__.print_function": "print_function", |
| "__future__.unicode_literals": "unicode_literals", |
| "__future__.barry_as_FLUFL": "barry_as_FLUFL", |
| "__future__.generator_stop": "generator_stop", |
| "__future__.annotations": "annotations", |
| } |
| |
| |
| # Special cased built-in classes that are needed for basic functionality and need to be |
| # available very early on. |
| CORE_BUILTIN_CLASSES: Final = ["object", "bool", "function"] |
| |
| |
| # Used for tracking incomplete references |
| Tag: _TypeAlias = int |
| |
| |
| class SemanticAnalyzer( |
| NodeVisitor[None], SemanticAnalyzerInterface, SemanticAnalyzerPluginInterface |
| ): |
| """Semantically analyze parsed mypy files. |
| |
| The analyzer binds names and does various consistency checks for an |
| AST. Note that type checking is performed as a separate pass. |
| """ |
| |
| __deletable__ = ["patches", "options", "cur_mod_node"] |
| |
| # Module name space |
| modules: dict[str, MypyFile] |
| # Global name space for current module |
| globals: SymbolTable |
| # Names declared using "global" (separate set for each scope) |
| global_decls: list[set[str]] |
| # Names declared using "nonlocal" (separate set for each scope) |
| nonlocal_decls: list[set[str]] |
| # Local names of function scopes; None for non-function scopes. |
| locals: list[SymbolTable | None] |
| # Whether each scope is a comprehension scope. |
| is_comprehension_stack: list[bool] |
| # Nested block depths of scopes |
| block_depth: list[int] |
| # TypeInfo of directly enclosing class (or None) |
| _type: TypeInfo | None = None |
| # Stack of outer classes (the second tuple item contains tvars). |
| type_stack: list[TypeInfo | None] |
| # Type variables bound by the current scope, be it class or function |
| tvar_scope: TypeVarLikeScope |
| # Per-module options |
| options: Options |
| |
| # Stack of functions being analyzed |
| function_stack: list[FuncItem] |
| |
| # Set to True if semantic analysis defines a name, or replaces a |
| # placeholder definition. If some iteration makes no progress, |
| # there can be at most one additional final iteration (see below). |
| progress = False |
| deferred = False # Set to true if another analysis pass is needed |
| incomplete = False # Set to true if current module namespace is missing things |
| # Is this the final iteration of semantic analysis (where we report |
| # unbound names due to cyclic definitions and should not defer)? |
| _final_iteration = False |
| # These names couldn't be added to the symbol table due to incomplete deps. |
| # Note that missing names are per module, _not_ per namespace. This means that e.g. |
| # a missing name at global scope will block adding same name at a class scope. |
| # This should not affect correctness and is purely a performance issue, |
| # since it can cause unnecessary deferrals. These are represented as |
| # PlaceholderNodes in the symbol table. We use this to ensure that the first |
| # definition takes precedence even if it's incomplete. |
| # |
| # Note that a star import adds a special name '*' to the set, this blocks |
| # adding _any_ names in the current file. |
| missing_names: list[set[str]] |
| # Callbacks that will be called after semantic analysis to tweak things. |
| patches: list[tuple[int, Callable[[], None]]] |
| loop_depth: list[int] # Depth of breakable loops |
| cur_mod_id = "" # Current module id (or None) (phase 2) |
| _is_stub_file = False # Are we analyzing a stub file? |
| _is_typeshed_stub_file = False # Are we analyzing a typeshed stub file? |
| imports: set[str] # Imported modules (during phase 2 analysis) |
| # Note: some imports (and therefore dependencies) might |
| # not be found in phase 1, for example due to * imports. |
| errors: Errors # Keeps track of generated errors |
| plugin: Plugin # Mypy plugin for special casing of library features |
| statement: Statement | None = None # Statement/definition being analyzed |
| |
| # Mapping from 'async def' function definitions to their return type wrapped as a |
| # 'Coroutine[Any, Any, T]'. Used to keep track of whether a function definition's |
| # return type has already been wrapped, by checking if the function definition's |
| # type is stored in this mapping and that it still matches. |
| wrapped_coro_return_types: dict[FuncDef, Type] = {} |
| |
| def __init__( |
| self, |
| modules: dict[str, MypyFile], |
| missing_modules: set[str], |
| incomplete_namespaces: set[str], |
| errors: Errors, |
| plugin: Plugin, |
| ) -> None: |
| """Construct semantic analyzer. |
| |
| We reuse the same semantic analyzer instance across multiple modules. |
| |
| Args: |
| modules: Global modules dictionary |
| missing_modules: Modules that could not be imported encountered so far |
| incomplete_namespaces: Namespaces that are being populated during semantic analysis |
| (can contain modules and classes within the current SCC; mutated by the caller) |
| errors: Report analysis errors using this instance |
| """ |
| self.locals = [None] |
| self.is_comprehension_stack = [False] |
| # Saved namespaces from previous iteration. Every top-level function/method body is |
| # analyzed in several iterations until all names are resolved. We need to save |
| # the local namespaces for the top level function and all nested functions between |
| # these iterations. See also semanal_main.process_top_level_function(). |
| self.saved_locals: dict[ |
| FuncItem | GeneratorExpr | DictionaryComprehension, SymbolTable |
| ] = {} |
| self.imports = set() |
| self._type = None |
| self.type_stack = [] |
| # Are the namespaces of classes being processed complete? |
| self.incomplete_type_stack: list[bool] = [] |
| self.tvar_scope = TypeVarLikeScope() |
| self.function_stack = [] |
| self.block_depth = [0] |
| self.loop_depth = [0] |
| self.errors = errors |
| self.modules = modules |
| self.msg = MessageBuilder(errors, modules) |
| self.missing_modules = missing_modules |
| self.missing_names = [set()] |
| # These namespaces are still in process of being populated. If we encounter a |
| # missing name in these namespaces, we need to defer the current analysis target, |
| # since it's possible that the name will be there once the namespace is complete. |
| self.incomplete_namespaces = incomplete_namespaces |
| self.all_exports: list[str] = [] |
| # Map from module id to list of explicitly exported names (i.e. names in __all__). |
| self.export_map: dict[str, list[str]] = {} |
| self.plugin = plugin |
| # If True, process function definitions. If False, don't. This is used |
| # for processing module top levels in fine-grained incremental mode. |
| self.recurse_into_functions = True |
| self.scope = Scope() |
| |
| # Trace line numbers for every file where deferral happened during analysis of |
| # current SCC or top-level function. |
| self.deferral_debug_context: list[tuple[str, int]] = [] |
| |
| # This is needed to properly support recursive type aliases. The problem is that |
| # Foo[Bar] could mean three things depending on context: a target for type alias, |
| # a normal index expression (including enum index), or a type application. |
| # The latter is particularly problematic as it can falsely create incomplete |
| # refs while analysing rvalues of type aliases. To avoid this we first analyse |
| # rvalues while temporarily setting this to True. |
| self.basic_type_applications = False |
| |
| # Used to temporarily enable unbound type variables in some contexts. Namely, |
| # in base class expressions, and in right hand sides of type aliases. Do not add |
| # new uses of this, as this may cause leaking `UnboundType`s to type checking. |
| self.allow_unbound_tvars = False |
| |
| # mypyc doesn't properly handle implementing an abstractproperty |
| # with a regular attribute so we make them properties |
| @property |
| def type(self) -> TypeInfo | None: |
| return self._type |
| |
| @property |
| def is_stub_file(self) -> bool: |
| return self._is_stub_file |
| |
| @property |
| def is_typeshed_stub_file(self) -> bool: |
| return self._is_typeshed_stub_file |
| |
| @property |
| def final_iteration(self) -> bool: |
| return self._final_iteration |
| |
| @contextmanager |
| def allow_unbound_tvars_set(self) -> Iterator[None]: |
| old = self.allow_unbound_tvars |
| self.allow_unbound_tvars = True |
| try: |
| yield |
| finally: |
| self.allow_unbound_tvars = old |
| |
| # |
| # Preparing module (performed before semantic analysis) |
| # |
| |
| def prepare_file(self, file_node: MypyFile) -> None: |
| """Prepare a freshly parsed file for semantic analysis.""" |
| if "builtins" in self.modules: |
| file_node.names["__builtins__"] = SymbolTableNode(GDEF, self.modules["builtins"]) |
| if file_node.fullname == "builtins": |
| self.prepare_builtins_namespace(file_node) |
| if file_node.fullname == "typing": |
| self.prepare_typing_namespace(file_node, type_aliases) |
| if file_node.fullname == "typing_extensions": |
| self.prepare_typing_namespace(file_node, typing_extensions_aliases) |
| |
| def prepare_typing_namespace(self, file_node: MypyFile, aliases: dict[str, str]) -> None: |
| """Remove dummy alias definitions such as List = TypeAlias(object) from typing. |
| |
| They will be replaced with real aliases when corresponding targets are ready. |
| """ |
| |
| # This is all pretty unfortunate. typeshed now has a |
| # sys.version_info check for OrderedDict, and we shouldn't |
| # take it out, because it is correct and a typechecker should |
| # use that as a source of truth. But instead we rummage |
| # through IfStmts to remove the info first. (I tried to |
| # remove this whole machinery and ran into issues with the |
| # builtins/typing import cycle.) |
| def helper(defs: list[Statement]) -> None: |
| for stmt in defs.copy(): |
| if isinstance(stmt, IfStmt): |
| for body in stmt.body: |
| helper(body.body) |
| if stmt.else_body: |
| helper(stmt.else_body.body) |
| if ( |
| isinstance(stmt, AssignmentStmt) |
| and len(stmt.lvalues) == 1 |
| and isinstance(stmt.lvalues[0], NameExpr) |
| ): |
| # Assignment to a simple name, remove it if it is a dummy alias. |
| if f"{file_node.fullname}.{stmt.lvalues[0].name}" in aliases: |
| defs.remove(stmt) |
| |
| helper(file_node.defs) |
| |
| def prepare_builtins_namespace(self, file_node: MypyFile) -> None: |
| """Add certain special-cased definitions to the builtins module. |
| |
| Some definitions are too special or fundamental to be processed |
| normally from the AST. |
| """ |
| names = file_node.names |
| |
| # Add empty definition for core built-in classes, since they are required for basic |
| # operation. These will be completed later on. |
| for name in CORE_BUILTIN_CLASSES: |
| cdef = ClassDef(name, Block([])) # Dummy ClassDef, will be replaced later |
| info = TypeInfo(SymbolTable(), cdef, "builtins") |
| info._fullname = f"builtins.{name}" |
| names[name] = SymbolTableNode(GDEF, info) |
| |
| bool_info = names["bool"].node |
| assert isinstance(bool_info, TypeInfo) |
| bool_type = Instance(bool_info, []) |
| |
| special_var_types: list[tuple[str, Type]] = [ |
| ("None", NoneType()), |
| # reveal_type is a mypy-only function that gives an error with |
| # the type of its arg. |
| ("reveal_type", AnyType(TypeOfAny.special_form)), |
| # reveal_locals is a mypy-only function that gives an error with the types of |
| # locals |
| ("reveal_locals", AnyType(TypeOfAny.special_form)), |
| ("True", bool_type), |
| ("False", bool_type), |
| ("__debug__", bool_type), |
| ] |
| |
| for name, typ in special_var_types: |
| v = Var(name, typ) |
| v._fullname = f"builtins.{name}" |
| file_node.names[name] = SymbolTableNode(GDEF, v) |
| |
| # |
| # Analyzing a target |
| # |
| |
| def refresh_partial( |
| self, |
| node: MypyFile | FuncDef | OverloadedFuncDef, |
| patches: list[tuple[int, Callable[[], None]]], |
| final_iteration: bool, |
| file_node: MypyFile, |
| options: Options, |
| active_type: TypeInfo | None = None, |
| ) -> None: |
| """Refresh a stale target in fine-grained incremental mode.""" |
| self.patches = patches |
| self.deferred = False |
| self.incomplete = False |
| self._final_iteration = final_iteration |
| self.missing_names[-1] = set() |
| |
| with self.file_context(file_node, options, active_type): |
| if isinstance(node, MypyFile): |
| self.refresh_top_level(node) |
| else: |
| self.recurse_into_functions = True |
| self.accept(node) |
| del self.patches |
| |
| def refresh_top_level(self, file_node: MypyFile) -> None: |
| """Reanalyze a stale module top-level in fine-grained incremental mode.""" |
| self.recurse_into_functions = False |
| self.add_implicit_module_attrs(file_node) |
| for d in file_node.defs: |
| self.accept(d) |
| if file_node.fullname == "typing": |
| self.add_builtin_aliases(file_node) |
| if file_node.fullname == "typing_extensions": |
| self.add_typing_extension_aliases(file_node) |
| self.adjust_public_exports() |
| self.export_map[self.cur_mod_id] = self.all_exports |
| self.all_exports = [] |
| |
| def add_implicit_module_attrs(self, file_node: MypyFile) -> None: |
| """Manually add implicit definitions of module '__name__' etc.""" |
| str_type: Type | None = self.named_type_or_none("builtins.str") |
| if str_type is None: |
| str_type = UnboundType("builtins.str") |
| for name, t in implicit_module_attrs.items(): |
| if name == "__doc__": |
| typ: Type = str_type |
| elif name == "__path__": |
| if not file_node.is_package_init_file(): |
| continue |
| # Need to construct the type ourselves, to avoid issues with __builtins__.list |
| # not being subscriptable or typing.List not getting bound |
| inst = self.named_type_or_none("builtins.list", [str_type]) |
| if inst is None: |
| assert not self.final_iteration, "Cannot find builtins.list to add __path__" |
| self.defer() |
| return |
| typ = inst |
| elif name == "__annotations__": |
| inst = self.named_type_or_none( |
| "builtins.dict", [str_type, AnyType(TypeOfAny.special_form)] |
| ) |
| if inst is None: |
| assert ( |
| not self.final_iteration |
| ), "Cannot find builtins.dict to add __annotations__" |
| self.defer() |
| return |
| typ = inst |
| else: |
| assert t is not None, f"type should be specified for {name}" |
| typ = UnboundType(t) |
| |
| existing = file_node.names.get(name) |
| if existing is not None and not isinstance(existing.node, PlaceholderNode): |
| # Already exists. |
| continue |
| |
| an_type = self.anal_type(typ) |
| if an_type: |
| var = Var(name, an_type) |
| var._fullname = self.qualified_name(name) |
| var.is_ready = True |
| self.add_symbol(name, var, dummy_context()) |
| else: |
| self.add_symbol( |
| name, |
| PlaceholderNode(self.qualified_name(name), file_node, -1), |
| dummy_context(), |
| ) |
| |
| def add_builtin_aliases(self, tree: MypyFile) -> None: |
| """Add builtin type aliases to typing module. |
| |
| For historical reasons, the aliases like `List = list` are not defined |
| in typeshed stubs for typing module. Instead we need to manually add the |
| corresponding nodes on the fly. We explicitly mark these aliases as normalized, |
| so that a user can write `typing.List[int]`. |
| """ |
| assert tree.fullname == "typing" |
| for alias, target_name in type_aliases.items(): |
| if type_aliases_source_versions[alias] > self.options.python_version: |
| # This alias is not available on this Python version. |
| continue |
| name = alias.split(".")[-1] |
| if name in tree.names and not isinstance(tree.names[name].node, PlaceholderNode): |
| continue |
| self.create_alias(tree, target_name, alias, name) |
| |
| def add_typing_extension_aliases(self, tree: MypyFile) -> None: |
| """Typing extensions module does contain some type aliases. |
| |
| We need to analyze them as such, because in typeshed |
| they are just defined as `_Alias()` call. |
| Which is not supported natively. |
| """ |
| assert tree.fullname == "typing_extensions" |
| |
| for alias, target_name in typing_extensions_aliases.items(): |
| name = alias.split(".")[-1] |
| if name in tree.names and isinstance(tree.names[name].node, TypeAlias): |
| continue # Do not reset TypeAliases on the second pass. |
| |
| # We need to remove any node that is there at the moment. It is invalid. |
| tree.names.pop(name, None) |
| |
| # Now, create a new alias. |
| self.create_alias(tree, target_name, alias, name) |
| |
| def create_alias(self, tree: MypyFile, target_name: str, alias: str, name: str) -> None: |
| tag = self.track_incomplete_refs() |
| n = self.lookup_fully_qualified_or_none(target_name) |
| if n: |
| if isinstance(n.node, PlaceholderNode): |
| self.mark_incomplete(name, tree) |
| else: |
| # Found built-in class target. Create alias. |
| target = self.named_type_or_none(target_name, []) |
| assert target is not None |
| # Transform List to List[Any], etc. |
| fix_instance_types(target, self.fail, self.note, self.options) |
| alias_node = TypeAlias( |
| target, |
| alias, |
| line=-1, |
| column=-1, # there is no context |
| no_args=True, |
| normalized=True, |
| ) |
| self.add_symbol(name, alias_node, tree) |
| elif self.found_incomplete_ref(tag): |
| # Built-in class target may not ready yet -- defer. |
| self.mark_incomplete(name, tree) |
| else: |
| # Test fixtures may be missing some builtin classes, which is okay. |
| # Kill the placeholder if there is one. |
| if name in tree.names: |
| assert isinstance(tree.names[name].node, PlaceholderNode) |
| del tree.names[name] |
| |
| def adjust_public_exports(self) -> None: |
| """Adjust the module visibility of globals due to __all__.""" |
| if "__all__" in self.globals: |
| for name, g in self.globals.items(): |
| # Being included in __all__ explicitly exports and makes public. |
| if name in self.all_exports: |
| g.module_public = True |
| g.module_hidden = False |
| # But when __all__ is defined, and a symbol is not included in it, |
| # it cannot be public. |
| else: |
| g.module_public = False |
| |
| @contextmanager |
| def file_context( |
| self, file_node: MypyFile, options: Options, active_type: TypeInfo | None = None |
| ) -> Iterator[None]: |
| """Configure analyzer for analyzing targets within a file/class. |
| |
| Args: |
| file_node: target file |
| options: options specific to the file |
| active_type: must be the surrounding class to analyze method targets |
| """ |
| scope = self.scope |
| self.options = options |
| self.errors.set_file(file_node.path, file_node.fullname, scope=scope, options=options) |
| self.cur_mod_node = file_node |
| self.cur_mod_id = file_node.fullname |
| with scope.module_scope(self.cur_mod_id): |
| self._is_stub_file = file_node.path.lower().endswith(".pyi") |
| self._is_typeshed_stub_file = is_typeshed_file( |
| options.abs_custom_typeshed_dir, file_node.path |
| ) |
| self.globals = file_node.names |
| self.tvar_scope = TypeVarLikeScope() |
| |
| self.named_tuple_analyzer = NamedTupleAnalyzer(options, self) |
| self.typed_dict_analyzer = TypedDictAnalyzer(options, self, self.msg) |
| self.enum_call_analyzer = EnumCallAnalyzer(options, self) |
| self.newtype_analyzer = NewTypeAnalyzer(options, self, self.msg) |
| |
| # Counter that keeps track of references to undefined things potentially caused by |
| # incomplete namespaces. |
| self.num_incomplete_refs = 0 |
| |
| if active_type: |
| self.incomplete_type_stack.append(False) |
| scope.enter_class(active_type) |
| self.enter_class(active_type.defn.info) |
| for tvar in active_type.defn.type_vars: |
| self.tvar_scope.bind_existing(tvar) |
| |
| yield |
| |
| if active_type: |
| scope.leave_class() |
| self.leave_class() |
| self._type = None |
| self.incomplete_type_stack.pop() |
| del self.options |
| |
| # |
| # Functions |
| # |
| |
| def visit_func_def(self, defn: FuncDef) -> None: |
| self.statement = defn |
| |
| # Visit default values because they may contain assignment expressions. |
| for arg in defn.arguments: |
| if arg.initializer: |
| arg.initializer.accept(self) |
| |
| defn.is_conditional = self.block_depth[-1] > 0 |
| |
| # Set full names even for those definitions that aren't added |
| # to a symbol table. For example, for overload items. |
| defn._fullname = self.qualified_name(defn.name) |
| |
| # We don't add module top-level functions to symbol tables |
| # when we analyze their bodies in the second phase on analysis, |
| # since they were added in the first phase. Nested functions |
| # get always added, since they aren't separate targets. |
| if not self.recurse_into_functions or len(self.function_stack) > 0: |
| if not defn.is_decorated and not defn.is_overload: |
| self.add_function_to_symbol_table(defn) |
| |
| if not self.recurse_into_functions: |
| return |
| |
| with self.scope.function_scope(defn): |
| self.analyze_func_def(defn) |
| |
| def analyze_func_def(self, defn: FuncDef) -> None: |
| self.function_stack.append(defn) |
| |
| if defn.type: |
| assert isinstance(defn.type, CallableType) |
| has_self_type = self.update_function_type_variables(defn.type, defn) |
| else: |
| has_self_type = False |
| |
| self.function_stack.pop() |
| |
| if self.is_class_scope(): |
| # Method definition |
| assert self.type is not None |
| defn.info = self.type |
| if defn.type is not None and defn.name in ("__init__", "__init_subclass__"): |
| assert isinstance(defn.type, CallableType) |
| if isinstance(get_proper_type(defn.type.ret_type), AnyType): |
| defn.type = defn.type.copy_modified(ret_type=NoneType()) |
| self.prepare_method_signature(defn, self.type, has_self_type) |
| |
| # Analyze function signature |
| with self.tvar_scope_frame(self.tvar_scope.method_frame()): |
| if defn.type: |
| self.check_classvar_in_signature(defn.type) |
| assert isinstance(defn.type, CallableType) |
| # Signature must be analyzed in the surrounding scope so that |
| # class-level imported names and type variables are in scope. |
| analyzer = self.type_analyzer() |
| tag = self.track_incomplete_refs() |
| result = analyzer.visit_callable_type(defn.type, nested=False) |
| # Don't store not ready types (including placeholders). |
| if self.found_incomplete_ref(tag) or has_placeholder(result): |
| self.defer(defn) |
| return |
| assert isinstance(result, ProperType) |
| if isinstance(result, CallableType): |
| # type guards need to have a positional argument, to spec |
| skip_self = self.is_class_scope() and not defn.is_static |
| if result.type_guard and ARG_POS not in result.arg_kinds[skip_self:]: |
| self.fail( |
| "TypeGuard functions must have a positional argument", |
| result, |
| code=codes.VALID_TYPE, |
| ) |
| # in this case, we just kind of just ... remove the type guard. |
| result = result.copy_modified(type_guard=None) |
| |
| result = self.remove_unpack_kwargs(defn, result) |
| if has_self_type and self.type is not None: |
| info = self.type |
| if info.self_type is not None: |
| result.variables = [info.self_type] + list(result.variables) |
| defn.type = result |
| self.add_type_alias_deps(analyzer.aliases_used) |
| self.check_function_signature(defn) |
| self.check_paramspec_definition(defn) |
| if isinstance(defn, FuncDef): |
| assert isinstance(defn.type, CallableType) |
| defn.type = set_callable_name(defn.type, defn) |
| |
| self.analyze_arg_initializers(defn) |
| self.analyze_function_body(defn) |
| |
| if self.is_class_scope(): |
| assert self.type is not None |
| # Mark protocol methods with empty bodies as implicitly abstract. |
| # This makes explicit protocol subclassing type-safe. |
| if ( |
| self.type.is_protocol |
| and not self.is_stub_file # Bodies in stub files are always empty. |
| and (not isinstance(self.scope.function, OverloadedFuncDef) or defn.is_property) |
| and defn.abstract_status != IS_ABSTRACT |
| and is_trivial_body(defn.body) |
| ): |
| defn.abstract_status = IMPLICITLY_ABSTRACT |
| if ( |
| is_trivial_body(defn.body) |
| and not self.is_stub_file |
| and defn.abstract_status != NOT_ABSTRACT |
| ): |
| defn.is_trivial_body = True |
| |
| if ( |
| defn.is_coroutine |
| and isinstance(defn.type, CallableType) |
| and self.wrapped_coro_return_types.get(defn) != defn.type |
| ): |
| if defn.is_async_generator: |
| # Async generator types are handled elsewhere |
| pass |
| else: |
| # A coroutine defined as `async def foo(...) -> T: ...` |
| # has external return type `Coroutine[Any, Any, T]`. |
| any_type = AnyType(TypeOfAny.special_form) |
| ret_type = self.named_type_or_none( |
| "typing.Coroutine", [any_type, any_type, defn.type.ret_type] |
| ) |
| assert ret_type is not None, "Internal error: typing.Coroutine not found" |
| defn.type = defn.type.copy_modified(ret_type=ret_type) |
| self.wrapped_coro_return_types[defn] = defn.type |
| |
| def remove_unpack_kwargs(self, defn: FuncDef, typ: CallableType) -> CallableType: |
| if not typ.arg_kinds or typ.arg_kinds[-1] is not ArgKind.ARG_STAR2: |
| return typ |
| last_type = typ.arg_types[-1] |
| if not isinstance(last_type, UnpackType): |
| return typ |
| last_type = get_proper_type(last_type.type) |
| if not isinstance(last_type, TypedDictType): |
| self.fail("Unpack item in ** argument must be a TypedDict", defn) |
| new_arg_types = typ.arg_types[:-1] + [AnyType(TypeOfAny.from_error)] |
| return typ.copy_modified(arg_types=new_arg_types) |
| overlap = set(typ.arg_names) & set(last_type.items) |
| # It is OK for TypedDict to have a key named 'kwargs'. |
| overlap.discard(typ.arg_names[-1]) |
| if overlap: |
| overlapped = ", ".join([f'"{name}"' for name in overlap]) |
| self.fail(f"Overlap between argument names and ** TypedDict items: {overlapped}", defn) |
| new_arg_types = typ.arg_types[:-1] + [AnyType(TypeOfAny.from_error)] |
| return typ.copy_modified(arg_types=new_arg_types) |
| # OK, everything looks right now, mark the callable type as using unpack. |
| new_arg_types = typ.arg_types[:-1] + [last_type] |
| return typ.copy_modified(arg_types=new_arg_types, unpack_kwargs=True) |
| |
| def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type: bool) -> None: |
| """Check basic signature validity and tweak annotation of self/cls argument.""" |
| # Only non-static methods are special, as well as __new__. |
| functype = func.type |
| if func.name == "__new__": |
| func.is_static = True |
| if not func.is_static or func.name == "__new__": |
| if func.name in ["__init_subclass__", "__class_getitem__"]: |
| func.is_class = True |
| if not func.arguments: |
| self.fail( |
| 'Method must have at least one argument. Did you forget the "self" argument?', |
| func, |
| ) |
| elif isinstance(functype, CallableType): |
| self_type = get_proper_type(functype.arg_types[0]) |
| if isinstance(self_type, AnyType): |
| if has_self_type: |
| assert self.type is not None and self.type.self_type is not None |
| leading_type: Type = self.type.self_type |
| else: |
| leading_type = fill_typevars(info) |
| if func.is_class or func.name == "__new__": |
| leading_type = self.class_type(leading_type) |
| func.type = replace_implicit_first_type(functype, leading_type) |
| elif has_self_type and isinstance(func.unanalyzed_type, CallableType): |
| if not isinstance(get_proper_type(func.unanalyzed_type.arg_types[0]), AnyType): |
| if self.is_expected_self_type( |
| self_type, func.is_class or func.name == "__new__" |
| ): |
| # This error is off by default, since it is explicitly allowed |
| # by the PEP 673. |
| self.fail( |
| 'Redundant "Self" annotation for the first method argument', |
| func, |
| code=codes.REDUNDANT_SELF_TYPE, |
| ) |
| else: |
| self.fail( |
| "Method cannot have explicit self annotation and Self type", func |
| ) |
| elif has_self_type: |
| self.fail("Static methods cannot use Self type", func) |
| |
| def is_expected_self_type(self, typ: Type, is_classmethod: bool) -> bool: |
| """Does this (analyzed or not) type represent the expected Self type for a method?""" |
| assert self.type is not None |
| typ = get_proper_type(typ) |
| if is_classmethod: |
| if isinstance(typ, TypeType): |
| return self.is_expected_self_type(typ.item, is_classmethod=False) |
| if isinstance(typ, UnboundType): |
| sym = self.lookup_qualified(typ.name, typ, suppress_errors=True) |
| if ( |
| sym is not None |
| and ( |
| sym.fullname == "typing.Type" |
| or ( |
| sym.fullname == "builtins.type" |
| and ( |
| self.is_stub_file |
| or self.is_future_flag_set("annotations") |
| or self.options.python_version >= (3, 9) |
| ) |
| ) |
| ) |
| and typ.args |
| ): |
| return self.is_expected_self_type(typ.args[0], is_classmethod=False) |
| return False |
| if isinstance(typ, TypeVarType): |
| return typ == self.type.self_type |
| if isinstance(typ, UnboundType): |
| sym = self.lookup_qualified(typ.name, typ, suppress_errors=True) |
| return sym is not None and sym.fullname in SELF_TYPE_NAMES |
| return False |
| |
| def set_original_def(self, previous: Node | None, new: FuncDef | Decorator) -> bool: |
| """If 'new' conditionally redefine 'previous', set 'previous' as original |
| |
| We reject straight redefinitions of functions, as they are usually |
| a programming error. For example: |
| |
| def f(): ... |
| def f(): ... # Error: 'f' redefined |
| """ |
| if isinstance(new, Decorator): |
| new = new.func |
| if ( |
| isinstance(previous, (FuncDef, Decorator)) |
| and unnamed_function(new.name) |
| and unnamed_function(previous.name) |
| ): |
| return True |
| if isinstance(previous, (FuncDef, Var, Decorator)) and new.is_conditional: |
| new.original_def = previous |
| return True |
| else: |
| return False |
| |
| def update_function_type_variables(self, fun_type: CallableType, defn: FuncItem) -> bool: |
| """Make any type variables in the signature of defn explicit. |
| |
| Update the signature of defn to contain type variable definitions |
| if defn is generic. Return True, if the signature contains typing.Self |
| type, or False otherwise. |
| """ |
| with self.tvar_scope_frame(self.tvar_scope.method_frame()): |
| a = self.type_analyzer() |
| fun_type.variables, has_self_type = a.bind_function_type_variables(fun_type, defn) |
| if has_self_type and self.type is not None: |
| self.setup_self_type() |
| return has_self_type |
| |
| def setup_self_type(self) -> None: |
| """Setup a (shared) Self type variable for current class. |
| |
| We intentionally don't add it to the class symbol table, |
| so it can be accessed only by mypy and will not cause |
| clashes with user defined names. |
| """ |
| assert self.type is not None |
| info = self.type |
| if info.self_type is not None: |
| if has_placeholder(info.self_type.upper_bound): |
| # Similar to regular (user defined) type variables. |
| self.process_placeholder( |
| None, |
| "Self upper bound", |
| info, |
| force_progress=info.self_type.upper_bound != fill_typevars(info), |
| ) |
| else: |
| return |
| info.self_type = TypeVarType( |
| "Self", |
| f"{info.fullname}.Self", |
| id=0, |
| values=[], |
| upper_bound=fill_typevars(info), |
| default=AnyType(TypeOfAny.from_omitted_generics), |
| ) |
| |
| def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: |
| self.statement = defn |
| self.add_function_to_symbol_table(defn) |
| |
| if not self.recurse_into_functions: |
| return |
| |
| # NB: Since _visit_overloaded_func_def will call accept on the |
| # underlying FuncDefs, the function might get entered twice. |
| # This is fine, though, because only the outermost function is |
| # used to compute targets. |
| with self.scope.function_scope(defn): |
| self.analyze_overloaded_func_def(defn) |
| |
| def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: |
| # OverloadedFuncDef refers to any legitimate situation where you have |
| # more than one declaration for the same function in a row. This occurs |
| # with a @property with a setter or a deleter, and for a classic |
| # @overload. |
| |
| defn._fullname = self.qualified_name(defn.name) |
| # TODO: avoid modifying items. |
| defn.items = defn.unanalyzed_items.copy() |
| |
| first_item = defn.items[0] |
| first_item.is_overload = True |
| first_item.accept(self) |
| |
| if isinstance(first_item, Decorator) and first_item.func.is_property: |
| # This is a property. |
| first_item.func.is_overload = True |
| self.analyze_property_with_multi_part_definition(defn) |
| typ = function_type(first_item.func, self.named_type("builtins.function")) |
| assert isinstance(typ, CallableType) |
| types = [typ] |
| else: |
| # This is an a normal overload. Find the item signatures, the |
| # implementation (if outside a stub), and any missing @overload |
| # decorators. |
| types, impl, non_overload_indexes = self.analyze_overload_sigs_and_impl(defn) |
| defn.impl = impl |
| if non_overload_indexes: |
| self.handle_missing_overload_decorators( |
| defn, non_overload_indexes, some_overload_decorators=len(types) > 0 |
| ) |
| # If we found an implementation, remove it from the overload item list, |
| # as it's special. |
| if impl is not None: |
| assert impl is defn.items[-1] |
| defn.items = defn.items[:-1] |
| elif not non_overload_indexes: |
| self.handle_missing_overload_implementation(defn) |
| |
| if types: |
| defn.type = Overloaded(types) |
| defn.type.line = defn.line |
| |
| if not defn.items: |
| # It was not a real overload after all, but function redefinition. We've |
| # visited the redefinition(s) already. |
| if not defn.impl: |
| # For really broken overloads with no items and no implementation we need to keep |
| # at least one item to hold basic information like function name. |
| defn.impl = defn.unanalyzed_items[-1] |
| return |
| |
| # We know this is an overload def. Infer properties and perform some checks. |
| self.process_final_in_overload(defn) |
| self.process_static_or_class_method_in_overload(defn) |
| self.process_overload_impl(defn) |
| |
| def process_overload_impl(self, defn: OverloadedFuncDef) -> None: |
| """Set flags for an overload implementation. |
| |
| Currently, this checks for a trivial body in protocols classes, |
| where it makes the method implicitly abstract. |
| """ |
| if defn.impl is None: |
| return |
| impl = defn.impl if isinstance(defn.impl, FuncDef) else defn.impl.func |
| if is_trivial_body(impl.body) and self.is_class_scope() and not self.is_stub_file: |
| assert self.type is not None |
| if self.type.is_protocol: |
| impl.abstract_status = IMPLICITLY_ABSTRACT |
| if impl.abstract_status != NOT_ABSTRACT: |
| impl.is_trivial_body = True |
| |
| def analyze_overload_sigs_and_impl( |
| self, defn: OverloadedFuncDef |
| ) -> tuple[list[CallableType], OverloadPart | None, list[int]]: |
| """Find overload signatures, the implementation, and items with missing @overload. |
| |
| Assume that the first was already analyzed. As a side effect: |
| analyzes remaining items and updates 'is_overload' flags. |
| """ |
| types = [] |
| non_overload_indexes = [] |
| impl: OverloadPart | None = None |
| for i, item in enumerate(defn.items): |
| if i != 0: |
| # Assume that the first item was already visited |
| item.is_overload = True |
| item.accept(self) |
| # TODO: support decorated overloaded functions properly |
| if isinstance(item, Decorator): |
| callable = function_type(item.func, self.named_type("builtins.function")) |
| assert isinstance(callable, CallableType) |
| if not any(refers_to_fullname(dec, OVERLOAD_NAMES) for dec in item.decorators): |
| if i == len(defn.items) - 1 and not self.is_stub_file: |
| # Last item outside a stub is impl |
| impl = item |
| else: |
| # Oops it wasn't an overload after all. A clear error |
| # will vary based on where in the list it is, record |
| # that. |
| non_overload_indexes.append(i) |
| else: |
| item.func.is_overload = True |
| types.append(callable) |
| if item.var.is_property: |
| self.fail("An overload can not be a property", item) |
| # If any item was decorated with `@override`, the whole overload |
| # becomes an explicit override. |
| defn.is_explicit_override |= item.func.is_explicit_override |
| elif isinstance(item, FuncDef): |
| if i == len(defn.items) - 1 and not self.is_stub_file: |
| impl = item |
| else: |
| non_overload_indexes.append(i) |
| return types, impl, non_overload_indexes |
| |
| def handle_missing_overload_decorators( |
| self, |
| defn: OverloadedFuncDef, |
| non_overload_indexes: list[int], |
| some_overload_decorators: bool, |
| ) -> None: |
| """Generate errors for overload items without @overload. |
| |
| Side effect: remote non-overload items. |
| """ |
| if some_overload_decorators: |
| # Some of them were overloads, but not all. |
| for idx in non_overload_indexes: |
| if self.is_stub_file: |
| self.fail( |
| "An implementation for an overloaded function " |
| "is not allowed in a stub file", |
| defn.items[idx], |
| ) |
| else: |
| self.fail( |
| "The implementation for an overloaded function must come last", |
| defn.items[idx], |
| ) |
| else: |
| for idx in non_overload_indexes[1:]: |
| self.name_already_defined(defn.name, defn.items[idx], defn.items[0]) |
| if defn.impl: |
| self.name_already_defined(defn.name, defn.impl, defn.items[0]) |
| # Remove the non-overloads |
| for idx in reversed(non_overload_indexes): |
| del defn.items[idx] |
| |
| def handle_missing_overload_implementation(self, defn: OverloadedFuncDef) -> None: |
| """Generate error about missing overload implementation (only if needed).""" |
| if not self.is_stub_file: |
| if self.type and self.type.is_protocol and not self.is_func_scope(): |
| # An overloaded protocol method doesn't need an implementation, |
| # but if it doesn't have one, then it is considered abstract. |
| for item in defn.items: |
| if isinstance(item, Decorator): |
| item.func.abstract_status = IS_ABSTRACT |
| else: |
| item.abstract_status = IS_ABSTRACT |
| else: |
| # TODO: also allow omitting an implementation for abstract methods in ABCs? |
| self.fail( |
| "An overloaded function outside a stub file must have an implementation", |
| defn, |
| code=codes.NO_OVERLOAD_IMPL, |
| ) |
| |
| def process_final_in_overload(self, defn: OverloadedFuncDef) -> None: |
| """Detect the @final status of an overloaded function (and perform checks).""" |
| # If the implementation is marked as @final (or the first overload in |
| # stubs), then the whole overloaded definition if @final. |
| if any(item.is_final for item in defn.items): |
| # We anyway mark it as final because it was probably the intention. |
| defn.is_final = True |
| # Only show the error once per overload |
| bad_final = next(ov for ov in defn.items if ov.is_final) |
| if not self.is_stub_file: |
| self.fail("@final should be applied only to overload implementation", bad_final) |
| elif any(item.is_final for item in defn.items[1:]): |
| bad_final = next(ov for ov in defn.items[1:] if ov.is_final) |
| self.fail( |
| "In a stub file @final must be applied only to the first overload", bad_final |
| ) |
| if defn.impl is not None and defn.impl.is_final: |
| defn.is_final = True |
| |
| def process_static_or_class_method_in_overload(self, defn: OverloadedFuncDef) -> None: |
| class_status = [] |
| static_status = [] |
| for item in defn.items: |
| if isinstance(item, Decorator): |
| inner = item.func |
| elif isinstance(item, FuncDef): |
| inner = item |
| else: |
| assert False, f"The 'item' variable is an unexpected type: {type(item)}" |
| class_status.append(inner.is_class) |
| static_status.append(inner.is_static) |
| |
| if defn.impl is not None: |
| if isinstance(defn.impl, Decorator): |
| inner = defn.impl.func |
| elif isinstance(defn.impl, FuncDef): |
| inner = defn.impl |
| else: |
| assert False, f"Unexpected impl type: {type(defn.impl)}" |
| class_status.append(inner.is_class) |
| static_status.append(inner.is_static) |
| |
| if len(set(class_status)) != 1: |
| self.msg.overload_inconsistently_applies_decorator("classmethod", defn) |
| elif len(set(static_status)) != 1: |
| self.msg.overload_inconsistently_applies_decorator("staticmethod", defn) |
| else: |
| defn.is_class = class_status[0] |
| defn.is_static = static_status[0] |
| |
| def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -> None: |
| """Analyze a property defined using multiple methods (e.g., using @x.setter). |
| |
| Assume that the first method (@property) has already been analyzed. |
| """ |
| defn.is_property = True |
| items = defn.items |
| first_item = defn.items[0] |
| assert isinstance(first_item, Decorator) |
| deleted_items = [] |
| for i, item in enumerate(items[1:]): |
| if isinstance(item, Decorator): |
| if len(item.decorators) >= 1: |
| node = item.decorators[0] |
| if isinstance(node, MemberExpr): |
| if node.name == "setter": |
| # The first item represents the entire property. |
| first_item.var.is_settable_property = True |
| # Get abstractness from the original definition. |
| item.func.abstract_status = first_item.func.abstract_status |
| if node.name == "deleter": |
| item.func.abstract_status = first_item.func.abstract_status |
| else: |
| self.fail( |
| f"Only supported top decorator is @{first_item.func.name}.setter", item |
| ) |
| item.func.accept(self) |
| else: |
| self.fail(f'Unexpected definition for property "{first_item.func.name}"', item) |
| deleted_items.append(i + 1) |
| for i in reversed(deleted_items): |
| del items[i] |
| |
| def add_function_to_symbol_table(self, func: FuncDef | OverloadedFuncDef) -> None: |
| if self.is_class_scope(): |
| assert self.type is not None |
| func.info = self.type |
| func._fullname = self.qualified_name(func.name) |
| self.add_symbol(func.name, func, func) |
| |
| def analyze_arg_initializers(self, defn: FuncItem) -> None: |
| with self.tvar_scope_frame(self.tvar_scope.method_frame()): |
| # Analyze default arguments |
| for arg in defn.arguments: |
| if arg.initializer: |
| arg.initializer.accept(self) |
| |
| def analyze_function_body(self, defn: FuncItem) -> None: |
| is_method = self.is_class_scope() |
| with self.tvar_scope_frame(self.tvar_scope.method_frame()): |
| # Bind the type variables again to visit the body. |
| if defn.type: |
| a = self.type_analyzer() |
| typ = defn.type |
| assert isinstance(typ, CallableType) |
| a.bind_function_type_variables(typ, defn) |
| for i in range(len(typ.arg_types)): |
| store_argument_type(defn, i, typ, self.named_type) |
| self.function_stack.append(defn) |
| with self.enter(defn): |
| for arg in defn.arguments: |
| self.add_local(arg.variable, defn) |
| |
| # The first argument of a non-static, non-class method is like 'self' |
| # (though the name could be different), having the enclosing class's |
| # instance type. |
| if is_method and (not defn.is_static or defn.name == "__new__") and defn.arguments: |
| if not defn.is_class: |
| defn.arguments[0].variable.is_self = True |
| else: |
| defn.arguments[0].variable.is_cls = True |
| |
| defn.body.accept(self) |
| self.function_stack.pop() |
| |
| def check_classvar_in_signature(self, typ: ProperType) -> None: |
| t: ProperType |
| if isinstance(typ, Overloaded): |
| for t in typ.items: |
| self.check_classvar_in_signature(t) |
| return |
| if not isinstance(typ, CallableType): |
| return |
| for t in get_proper_types(typ.arg_types) + [get_proper_type(typ.ret_type)]: |
| if self.is_classvar(t): |
| self.fail_invalid_classvar(t) |
| # Show only one error per signature |
| break |
| |
| def check_function_signature(self, fdef: FuncItem) -> None: |
| sig = fdef.type |
| assert isinstance(sig, CallableType) |
| if len(sig.arg_types) < len(fdef.arguments): |
| self.fail("Type signature has too few arguments", fdef) |
| # Add dummy Any arguments to prevent crashes later. |
| num_extra_anys = len(fdef.arguments) - len(sig.arg_types) |
| extra_anys = [AnyType(TypeOfAny.from_error)] * num_extra_anys |
| sig.arg_types.extend(extra_anys) |
| elif len(sig.arg_types) > len(fdef.arguments): |
| self.fail("Type signature has too many arguments", fdef, blocker=True) |
| |
| def check_paramspec_definition(self, defn: FuncDef) -> None: |
| func = defn.type |
| assert isinstance(func, CallableType) |
| |
| if not any(isinstance(var, ParamSpecType) for var in func.variables): |
| return # Function does not have param spec variables |
| |
| args = func.var_arg() |
| kwargs = func.kw_arg() |
| if args is None and kwargs is None: |
| return # Looks like this function does not have starred args |
| |
| args_defn_type = None |
| kwargs_defn_type = None |
| for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds): |
| if arg_kind == ARG_STAR: |
| args_defn_type = arg_def.type_annotation |
| elif arg_kind == ARG_STAR2: |
| kwargs_defn_type = arg_def.type_annotation |
| |
| # This may happen on invalid `ParamSpec` args / kwargs definition, |
| # type analyzer sets types of arguments to `Any`, but keeps |
| # definition types as `UnboundType` for now. |
| if not ( |
| (isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args")) |
| or ( |
| isinstance(kwargs_defn_type, UnboundType) |
| and kwargs_defn_type.name.endswith(".kwargs") |
| ) |
| ): |
| # Looks like both `*args` and `**kwargs` are not `ParamSpec` |
| # It might be something else, skipping. |
| return |
| |
| args_type = args.typ if args is not None else None |
| kwargs_type = kwargs.typ if kwargs is not None else None |
| |
| if ( |
| not isinstance(args_type, ParamSpecType) |
| or not isinstance(kwargs_type, ParamSpecType) |
| or args_type.name != kwargs_type.name |
| ): |
| if isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"): |
| param_name = args_defn_type.name.split(".")[0] |
| elif isinstance(kwargs_defn_type, UnboundType) and kwargs_defn_type.name.endswith( |
| ".kwargs" |
| ): |
| param_name = kwargs_defn_type.name.split(".")[0] |
| else: |
| # Fallback for cases that probably should not ever happen: |
| param_name = "P" |
| |
| self.fail( |
| f'ParamSpec must have "*args" typed as "{param_name}.args" and "**kwargs" typed as "{param_name}.kwargs"', |
| func, |
| code=codes.VALID_TYPE, |
| ) |
| |
| def visit_decorator(self, dec: Decorator) -> None: |
| self.statement = dec |
| # TODO: better don't modify them at all. |
| dec.decorators = dec.original_decorators.copy() |
| dec.func.is_conditional = self.block_depth[-1] > 0 |
| if not dec.is_overload: |
| self.add_symbol(dec.name, dec, dec) |
| dec.func._fullname = self.qualified_name(dec.name) |
| dec.var._fullname = self.qualified_name(dec.name) |
| for d in dec.decorators: |
| d.accept(self) |
| removed: list[int] = [] |
| no_type_check = False |
| could_be_decorated_property = False |
| for i, d in enumerate(dec.decorators): |
| # A bunch of decorators are special cased here. |
| if refers_to_fullname(d, "abc.abstractmethod"): |
| removed.append(i) |
| dec.func.abstract_status = IS_ABSTRACT |
| self.check_decorated_function_is_method("abstractmethod", dec) |
| elif refers_to_fullname(d, ("asyncio.coroutines.coroutine", "types.coroutine")): |
| removed.append(i) |
| dec.func.is_awaitable_coroutine = True |
| elif refers_to_fullname(d, "builtins.staticmethod"): |
| removed.append(i) |
| dec.func.is_static = True |
| dec.var.is_staticmethod = True |
| self.check_decorated_function_is_method("staticmethod", dec) |
| elif refers_to_fullname(d, "builtins.classmethod"): |
| removed.append(i) |
| dec.func.is_class = True |
| dec.var.is_classmethod = True |
| self.check_decorated_function_is_method("classmethod", dec) |
| elif refers_to_fullname(d, OVERRIDE_DECORATOR_NAMES): |
| removed.append(i) |
| dec.func.is_explicit_override = True |
| self.check_decorated_function_is_method("override", dec) |
| elif refers_to_fullname( |
| d, |
| ( |
| "builtins.property", |
| "abc.abstractproperty", |
| "functools.cached_property", |
| "enum.property", |
| ), |
| ): |
| removed.append(i) |
| dec.func.is_property = True |
| dec.var.is_property = True |
| if refers_to_fullname(d, "abc.abstractproperty"): |
| dec.func.abstract_status = IS_ABSTRACT |
| elif refers_to_fullname(d, "functools.cached_property"): |
| dec.var.is_settable_property = True |
| self.check_decorated_function_is_method("property", dec) |
| elif refers_to_fullname(d, "typing.no_type_check"): |
| dec.var.type = AnyType(TypeOfAny.special_form) |
| no_type_check = True |
| elif refers_to_fullname(d, FINAL_DECORATOR_NAMES): |
| if self.is_class_scope(): |
| assert self.type is not None, "No type set at class scope" |
| if self.type.is_protocol: |
| self.msg.protocol_members_cant_be_final(d) |
| else: |
| dec.func.is_final = True |
| dec.var.is_final = True |
| removed.append(i) |
| else: |
| self.fail("@final cannot be used with non-method functions", d) |
| elif isinstance(d, CallExpr) and refers_to_fullname( |
| d.callee, DATACLASS_TRANSFORM_NAMES |
| ): |
| dec.func.dataclass_transform_spec = self.parse_dataclass_transform_spec(d) |
| elif not dec.var.is_property: |
| # We have seen a "non-trivial" decorator before seeing @property, if |
| # we will see a @property later, give an error, as we don't support this. |
| could_be_decorated_property = True |
| for i in reversed(removed): |
| del dec.decorators[i] |
| if (not dec.is_overload or dec.var.is_property) and self.type: |
| dec.var.info = self.type |
| dec.var.is_initialized_in_class = True |
| if not no_type_check and self.recurse_into_functions: |
| dec.func.accept(self) |
| if could_be_decorated_property and dec.decorators and dec.var.is_property: |
| self.fail("Decorators on top of @property are not supported", dec) |
| if (dec.func.is_static or dec.func.is_class) and dec.var.is_property: |
| self.fail("Only instance methods can be decorated with @property", dec) |
| if dec.func.abstract_status == IS_ABSTRACT and dec.func.is_final: |
| self.fail(f"Method {dec.func.name} is both abstract and final", dec) |
| if dec.func.is_static and dec.func.is_class: |
| self.fail(message_registry.CLASS_PATTERN_CLASS_OR_STATIC_METHOD, dec) |
| |
| def check_decorated_function_is_method(self, decorator: str, context: Context) -> None: |
| if not self.type or self.is_func_scope(): |
| self.fail(f'"{decorator}" used with a non-method', context) |
| |
| # |
| # Classes |
| # |
| |
| def visit_class_def(self, defn: ClassDef) -> None: |
| self.statement = defn |
| self.incomplete_type_stack.append(not defn.info) |
| namespace = self.qualified_name(defn.name) |
| with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): |
| self.analyze_class(defn) |
| self.incomplete_type_stack.pop() |
| |
| def analyze_class(self, defn: ClassDef) -> None: |
| fullname = self.qualified_name(defn.name) |
| if not defn.info and not self.is_core_builtin_class(defn): |
| # Add placeholder so that self-references in base classes can be |
| # resolved. We don't want this to cause a deferral, since if there |
| # are no incomplete references, we'll replace this with a TypeInfo |
| # before returning. |
| placeholder = PlaceholderNode(fullname, defn, defn.line, becomes_typeinfo=True) |
| self.add_symbol(defn.name, placeholder, defn, can_defer=False) |
| |
| tag = self.track_incomplete_refs() |
| |
| # Restore base classes after previous iteration (things like Generic[T] might be removed). |
| defn.base_type_exprs.extend(defn.removed_base_type_exprs) |
| defn.removed_base_type_exprs.clear() |
| |
| self.infer_metaclass_and_bases_from_compat_helpers(defn) |
| |
| bases = defn.base_type_exprs |
| bases, tvar_defs, is_protocol = self.clean_up_bases_and_infer_type_variables( |
| defn, bases, context=defn |
| ) |
| |
| for tvd in tvar_defs: |
| if isinstance(tvd, TypeVarType) and any( |
| has_placeholder(t) for t in [tvd.upper_bound] + tvd.values |
| ): |
| # Some type variable bounds or values are not ready, we need |
| # to re-analyze this class. |
| self.defer() |
| if has_placeholder(tvd.default): |
| # Placeholder values in TypeVarLikeTypes may get substituted in. |
| # Defer current target until they are ready. |
| self.mark_incomplete(defn.name, defn) |
| return |
| |
| self.analyze_class_keywords(defn) |
| bases_result = self.analyze_base_classes(bases) |
| if bases_result is None or self.found_incomplete_ref(tag): |
| # Something was incomplete. Defer current target. |
| self.mark_incomplete(defn.name, defn) |
| return |
| |
| base_types, base_error = bases_result |
| if any(isinstance(base, PlaceholderType) for base, _ in base_types): |
| # We need to know the TypeInfo of each base to construct the MRO. Placeholder types |
| # are okay in nested positions, since they can't affect the MRO. |
| self.mark_incomplete(defn.name, defn) |
| return |
| |
| declared_metaclass, should_defer, any_meta = self.get_declared_metaclass( |
| defn.name, defn.metaclass |
| ) |
| if should_defer or self.found_incomplete_ref(tag): |
| # Metaclass was not ready. Defer current target. |
| self.mark_incomplete(defn.name, defn) |
| return |
| |
| if self.analyze_typeddict_classdef(defn): |
| if defn.info: |
| self.setup_type_vars(defn, tvar_defs) |
| self.setup_alias_type_vars(defn) |
| return |
| |
| if self.analyze_namedtuple_classdef(defn, tvar_defs): |
| return |
| |
| # Create TypeInfo for class now that base classes and the MRO can be calculated. |
| self.prepare_class_def(defn) |
| self.setup_type_vars(defn, tvar_defs) |
| if base_error: |
| defn.info.fallback_to_any = True |
| if any_meta: |
| defn.info.meta_fallback_to_any = True |
| |
| with self.scope.class_scope(defn.info): |
| self.configure_base_classes(defn, base_types) |
| defn.info.is_protocol = is_protocol |
| self.recalculate_metaclass(defn, declared_metaclass) |
| defn.info.runtime_protocol = False |
| for decorator in defn.decorators: |
| self.analyze_class_decorator(defn, decorator) |
| self.analyze_class_body_common(defn) |
| |
| def setup_type_vars(self, defn: ClassDef, tvar_defs: list[TypeVarLikeType]) -> None: |
| defn.type_vars = tvar_defs |
| defn.info.type_vars = [] |
| # we want to make sure any additional logic in add_type_vars gets run |
| defn.info.add_type_vars() |
| |
| def setup_alias_type_vars(self, defn: ClassDef) -> None: |
| assert defn.info.special_alias is not None |
| defn.info.special_alias.alias_tvars = list(defn.type_vars) |
| target = defn.info.special_alias.target |
| assert isinstance(target, ProperType) |
| if isinstance(target, TypedDictType): |
| target.fallback.args = tuple(defn.type_vars) |
| elif isinstance(target, TupleType): |
| target.partial_fallback.args = tuple(defn.type_vars) |
| else: |
| assert False, f"Unexpected special alias type: {type(target)}" |
| |
| def is_core_builtin_class(self, defn: ClassDef) -> bool: |
| return self.cur_mod_id == "builtins" and defn.name in CORE_BUILTIN_CLASSES |
| |
| def analyze_class_body_common(self, defn: ClassDef) -> None: |
| """Parts of class body analysis that are common to all kinds of class defs.""" |
| self.enter_class(defn.info) |
| if any(b.self_type is not None for b in defn.info.mro): |
| self.setup_self_type() |
| defn.defs.accept(self) |
| self.apply_class_plugin_hooks(defn) |
| self.leave_class() |
| |
| def analyze_typeddict_classdef(self, defn: ClassDef) -> bool: |
| if ( |
| defn.info |
| and defn.info.typeddict_type |
| and not has_placeholder(defn.info.typeddict_type) |
| ): |
| # This is a valid TypedDict, and it is fully analyzed. |
| return True |
| is_typeddict, info = self.typed_dict_analyzer.analyze_typeddict_classdef(defn) |
| if is_typeddict: |
| for decorator in defn.decorators: |
| decorator.accept(self) |
| if isinstance(decorator, RefExpr): |
| if decorator.fullname in FINAL_DECORATOR_NAMES and info is not None: |
| info.is_final = True |
| if info is None: |
| self.mark_incomplete(defn.name, defn) |
| else: |
| self.prepare_class_def(defn, info) |
| return True |
| return False |
| |
| def analyze_namedtuple_classdef( |
| self, defn: ClassDef, tvar_defs: list[TypeVarLikeType] |
| ) -> bool: |
| """Check if this class can define a named tuple.""" |
| if ( |
| defn.info |
| and defn.info.is_named_tuple |
| and defn.info.tuple_type |
| and not has_placeholder(defn.info.tuple_type) |
| ): |
| # Don't reprocess everything. We just need to process methods defined |
| # in the named tuple class body. |
| is_named_tuple = True |
| info: TypeInfo | None = defn.info |
| else: |
| is_named_tuple, info = self.named_tuple_analyzer.analyze_namedtuple_classdef( |
| defn, self.is_stub_file, self.is_func_scope() |
| ) |
| if is_named_tuple: |
| if info is None: |
| self.mark_incomplete(defn.name, defn) |
| else: |
| self.prepare_class_def(defn, info, custom_names=True) |
| self.setup_type_vars(defn, tvar_defs) |
| self.setup_alias_type_vars(defn) |
| with self.scope.class_scope(defn.info): |
| for deco in defn.decorators: |
| deco.accept(self) |
| if isinstance(deco, RefExpr) and deco.fullname in FINAL_DECORATOR_NAMES: |
| info.is_final = True |
| with self.named_tuple_analyzer.save_namedtuple_body(info): |
| self.analyze_class_body_common(defn) |
| return True |
| return False |
| |
| def apply_class_plugin_hooks(self, defn: ClassDef) -> None: |
| """Apply a plugin hook that may infer a more precise definition for a class.""" |
| |
| for decorator in defn.decorators: |
| decorator_name = self.get_fullname_for_hook(decorator) |
| if decorator_name: |
| hook = self.plugin.get_class_decorator_hook(decorator_name) |
| # Special case: if the decorator is itself decorated with |
| # typing.dataclass_transform, apply the hook for the dataclasses plugin |
| # TODO: remove special casing here |
| if hook is None and find_dataclass_transform_spec(decorator): |
| hook = dataclasses_plugin.dataclass_tag_callback |
| if hook: |
| hook(ClassDefContext(defn, decorator, self)) |
| |
| if defn.metaclass: |
| metaclass_name = self.get_fullname_for_hook(defn.metaclass) |
| if metaclass_name: |
| hook = self.plugin.get_metaclass_hook(metaclass_name) |
| if hook: |
| hook(ClassDefContext(defn, defn.metaclass, self)) |
| |
| for base_expr in defn.base_type_exprs: |
| base_name = self.get_fullname_for_hook(base_expr) |
| if base_name: |
| hook = self.plugin.get_base_class_hook(base_name) |
| if hook: |
| hook(ClassDefContext(defn, base_expr, self)) |
| |
| # Check if the class definition itself triggers a dataclass transform (via a parent class/ |
| # metaclass) |
| spec = find_dataclass_transform_spec(defn) |
| if spec is not None: |
| dataclasses_plugin.add_dataclass_tag(defn.info) |
| |
| def get_fullname_for_hook(self, expr: Expression) -> str | None: |
| if isinstance(expr, CallExpr): |
| return self.get_fullname_for_hook(expr.callee) |
| elif isinstance(expr, IndexExpr): |
| return self.get_fullname_for_hook(expr.base) |
| elif isinstance(expr, RefExpr): |
| if expr.fullname: |
| return expr.fullname |
| # If we don't have a fullname look it up. This happens because base classes are |
| # analyzed in a different manner (see exprtotype.py) and therefore those AST |
| # nodes will not have full names. |
| sym = self.lookup_type_node(expr) |
| if sym: |
| return sym.fullname |
| return None |
| |
| def analyze_class_keywords(self, defn: ClassDef) -> None: |
| for value in defn.keywords.values(): |
| value.accept(self) |
| |
| def enter_class(self, info: TypeInfo) -> None: |
| # Remember previous active class |
| self.type_stack.append(self.type) |
| self.locals.append(None) # Add class scope |
| self.is_comprehension_stack.append(False) |
| self.block_depth.append(-1) # The class body increments this to 0 |
| self.loop_depth.append(0) |
| self._type = info |
| self.missing_names.append(set()) |
| |
| def leave_class(self) -> None: |
| """Restore analyzer state.""" |
| self.block_depth.pop() |
| self.loop_depth.pop() |
| self.locals.pop() |
| self.is_comprehension_stack.pop() |
| self._type = self.type_stack.pop() |
| self.missing_names.pop() |
| |
| def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None: |
| decorator.accept(self) |
| if isinstance(decorator, RefExpr): |
| if decorator.fullname in RUNTIME_PROTOCOL_DECOS: |
| if defn.info.is_protocol: |
| defn.info.runtime_protocol = True |
| else: |
| self.fail("@runtime_checkable can only be used with protocol classes", defn) |
| elif decorator.fullname in FINAL_DECORATOR_NAMES: |
| defn.info.is_final = True |
| elif isinstance(decorator, CallExpr) and refers_to_fullname( |
| decorator.callee, DATACLASS_TRANSFORM_NAMES |
| ): |
| defn.info.dataclass_transform_spec = self.parse_dataclass_transform_spec(decorator) |
| |
| def clean_up_bases_and_infer_type_variables( |
| self, defn: ClassDef, base_type_exprs: list[Expression], context: Context |
| ) -> tuple[list[Expression], list[TypeVarLikeType], bool]: |
| """Remove extra base classes such as Generic and infer type vars. |
| |
| For example, consider this class: |
| |
| class Foo(Bar, Generic[T]): ... |
| |
| Now we will remove Generic[T] from bases of Foo and infer that the |
| type variable 'T' is a type argument of Foo. |
| |
| Note that this is performed *before* semantic analysis. |
| |
| Returns (remaining base expressions, inferred type variables, is protocol). |
| """ |
| removed: list[int] = [] |
| declared_tvars: TypeVarLikeList = [] |
| is_protocol = False |
| for i, base_expr in enumerate(base_type_exprs): |
| if isinstance(base_expr, StarExpr): |
| base_expr.valid = True |
| self.analyze_type_expr(base_expr) |
| |
| try: |
| base = self.expr_to_unanalyzed_type(base_expr) |
| except TypeTranslationError: |
| # This error will be caught later. |
| continue |
| result = self.analyze_class_typevar_declaration(base) |
| if result is not None: |
| if declared_tvars: |
| self.fail("Only single Generic[...] or Protocol[...] can be in bases", context) |
| removed.append(i) |
| tvars = result[0] |
| is_protocol |= result[1] |
| declared_tvars.extend(tvars) |
| if isinstance(base, UnboundType): |
| sym = self.lookup_qualified(base.name, base) |
| if sym is not None and sym.node is not None: |
| if sym.node.fullname in PROTOCOL_NAMES and i not in removed: |
| # also remove bare 'Protocol' bases |
| removed.append(i) |
| is_protocol = True |
| |
| all_tvars = self.get_all_bases_tvars(base_type_exprs, removed) |
| if declared_tvars: |
| if len(remove_dups(declared_tvars)) < len(declared_tvars): |
| self.fail("Duplicate type variables in Generic[...] or Protocol[...]", context) |
| declared_tvars = remove_dups(declared_tvars) |
| if not set(all_tvars).issubset(set(declared_tvars)): |
| self.fail( |
| "If Generic[...] or Protocol[...] is present" |
| " it should list all type variables", |
| context, |
| ) |
| # In case of error, Generic tvars will go first |
| declared_tvars = remove_dups(declared_tvars + all_tvars) |
| else: |
| declared_tvars = all_tvars |
| for i in reversed(removed): |
| # We need to actually remove the base class expressions like Generic[T], |
| # mostly because otherwise they will create spurious dependencies in fine |
| # grained incremental mode. |
| defn.removed_base_type_exprs.append(defn.base_type_exprs[i]) |
| del base_type_exprs[i] |
| tvar_defs: list[TypeVarLikeType] = [] |
| for name, tvar_expr in declared_tvars: |
| tvar_def = self.tvar_scope.bind_new(name, tvar_expr) |
| tvar_defs.append(tvar_def) |
| return base_type_exprs, tvar_defs, is_protocol |
| |
| def analyze_class_typevar_declaration(self, base: Type) -> tuple[TypeVarLikeList, bool] | None: |
| """Analyze type variables declared using Generic[...] or Protocol[...]. |
| |
| Args: |
| base: Non-analyzed base class |
| |
| Return None if the base class does not declare type variables. Otherwise, |
| return the type variables. |
| """ |
| if not isinstance(base, UnboundType): |
| return None |
| unbound = base |
| sym = self.lookup_qualified(unbound.name, unbound) |
| if sym is None or sym.node is None: |
| return None |
| if ( |
| sym.node.fullname == "typing.Generic" |
| or sym.node.fullname in PROTOCOL_NAMES |
| and base.args |
| ): |
| is_proto = sym.node.fullname != "typing.Generic" |
| tvars: TypeVarLikeList = [] |
| have_type_var_tuple = False |
| for arg in unbound.args: |
| tag = self.track_incomplete_refs() |
| tvar = self.analyze_unbound_tvar(arg) |
| if tvar: |
| if isinstance(tvar[1], TypeVarTupleExpr): |
| if have_type_var_tuple: |
| self.fail("Can only use one type var tuple in a class def", base) |
| continue |
| have_type_var_tuple = True |
| tvars.append(tvar) |
| elif not self.found_incomplete_ref(tag): |
| self.fail("Free type variable expected in %s[...]" % sym.node.name, base) |
| return tvars, is_proto |
| return None |
| |
| def analyze_unbound_tvar(self, t: Type) -> tuple[str, TypeVarLikeExpr] | None: |
| if not isinstance(t, UnboundType): |
| return None |
| unbound = t |
| sym = self.lookup_qualified(unbound.name, unbound) |
| if sym and isinstance(sym.node, PlaceholderNode): |
| self.record_incomplete_ref() |
| if sym and isinstance(sym.node, ParamSpecExpr): |
| if sym.fullname and not self.tvar_scope.allow_binding(sym.fullname): |
| # It's bound by our type variable scope |
| return None |
| return unbound.name, sym.node |
| if sym and sym.fullname in ("typing.Unpack", "typing_extensions.Unpack"): |
| inner_t = unbound.args[0] |
| if not isinstance(inner_t, UnboundType): |
| return None |
| inner_unbound = inner_t |
| inner_sym = self.lookup_qualified(inner_unbound.name, inner_unbound) |
| if inner_sym and isinstance(inner_sym.node, PlaceholderNode): |
| self.record_incomplete_ref() |
| if inner_sym and isinstance(inner_sym.node, TypeVarTupleExpr): |
| if inner_sym.fullname and not self.tvar_scope.allow_binding(inner_sym.fullname): |
| # It's bound by our type variable scope |
| return None |
| return inner_unbound.name, inner_sym.node |
| if sym is None or not isinstance(sym.node, TypeVarExpr): |
| return None |
| elif sym.fullname and not self.tvar_scope.allow_binding(sym.fullname): |
| # It's bound by our type variable scope |
| return None |
| else: |
| assert isinstance(sym.node, TypeVarExpr) |
| return unbound.name, sym.node |
| |
| def get_all_bases_tvars( |
| self, base_type_exprs: list[Expression], removed: list[int] |
| ) -> TypeVarLikeList: |
| """Return all type variable references in bases.""" |
| tvars: TypeVarLikeList = [] |
| for i, base_expr in enumerate(base_type_exprs): |
| if i not in removed: |
| try: |
| base = self.expr_to_unanalyzed_type(base_expr) |
| except TypeTranslationError: |
| # This error will be caught later. |
| continue |
| base_tvars = base.accept(TypeVarLikeQuery(self, self.tvar_scope)) |
| tvars.extend(base_tvars) |
| return remove_dups(tvars) |
| |
| def get_and_bind_all_tvars(self, type_exprs: list[Expression]) -> list[TypeVarLikeType]: |
| """Return all type variable references in item type expressions. |
| |
| This is a helper for generic TypedDicts and NamedTuples. Essentially it is |
| a simplified version of the logic we use for ClassDef bases. We duplicate |
| some amount of code, because it is hard to refactor common pieces. |
| """ |
| tvars = [] |
| for base_expr in type_exprs: |
| try: |
| base = self.expr_to_unanalyzed_type(base_expr) |
| except TypeTranslationError: |
| # This error will be caught later. |
| continue |
| base_tvars = base.accept(TypeVarLikeQuery(self, self.tvar_scope)) |
| tvars.extend(base_tvars) |
| tvars = remove_dups(tvars) # Variables are defined in order of textual appearance. |
| tvar_defs = [] |
| for name, tvar_expr in tvars: |
| tvar_def = self.tvar_scope.bind_new(name, tvar_expr) |
| tvar_defs.append(tvar_def) |
| return tvar_defs |
| |
| def prepare_class_def( |
| self, defn: ClassDef, info: TypeInfo | None = None, custom_names: bool = False |
| ) -> None: |
| """Prepare for the analysis of a class definition. |
| |
| Create an empty TypeInfo and store it in a symbol table, or if the 'info' |
| argument is provided, store it instead (used for magic type definitions). |
| """ |
| if not defn.info: |
| defn.fullname = self.qualified_name(defn.name) |
| # TODO: Nested classes |
| info = info or self.make_empty_type_info(defn) |
| defn.info = info |
| info.defn = defn |
| if not custom_names: |
| # Some special classes (in particular NamedTuples) use custom fullname logic. |
| # Don't override it here (also see comment below, this needs cleanup). |
| if not self.is_func_scope(): |
| info._fullname = self.qualified_name(defn.name) |
| else: |
| info._fullname = info.name |
| local_name = defn.name |
| if "@" in local_name: |
| local_name = local_name.split("@")[0] |
| self.add_symbol(local_name, defn.info, defn) |
| if self.is_nested_within_func_scope(): |
| # We need to preserve local classes, let's store them |
| # in globals under mangled unique names |
| # |
| # TODO: Putting local classes into globals breaks assumptions in fine-grained |
| # incremental mode and we should avoid it. In general, this logic is too |
| # ad-hoc and needs to be removed/refactored. |
| if "@" not in defn.info._fullname: |
| global_name = defn.info.name + "@" + str(defn.line) |
| defn.info._fullname = self.cur_mod_id + "." + global_name |
| else: |
| # Preserve name from previous fine-grained incremental run. |
| global_name = defn.info.name |
| defn.fullname = defn.info._fullname |
| if defn.info.is_named_tuple: |
| # Named tuple nested within a class is stored in the class symbol table. |
| self.add_symbol_skip_local(global_name, defn.info) |
| else: |
| self.globals[global_name] = SymbolTableNode(GDEF, defn.info) |
| |
| def make_empty_type_info(self, defn: ClassDef) -> TypeInfo: |
| if ( |
| self.is_module_scope() |
| and self.cur_mod_id == "builtins" |
| and defn.name in CORE_BUILTIN_CLASSES |
| ): |
| # Special case core built-in classes. A TypeInfo was already |
| # created for it before semantic analysis, but with a dummy |
| # ClassDef. Patch the real ClassDef object. |
| info = self.globals[defn.name].node |
| assert isinstance(info, TypeInfo) |
| else: |
| info = TypeInfo(SymbolTable(), defn, self.cur_mod_id) |
| info.set_line(defn) |
| return info |
| |
| def get_name_repr_of_expr(self, expr: Expression) -> str | None: |
| """Try finding a short simplified textual representation of a base class expression.""" |
| if isinstance(expr, NameExpr): |
| return expr.name |
| if isinstance(expr, MemberExpr): |
| return get_member_expr_fullname(expr) |
| if isinstance(expr, IndexExpr): |
| return self.get_name_repr_of_expr(expr.base) |
| if isinstance(expr, CallExpr): |
| return self.get_name_repr_of_expr(expr.callee) |
| return None |
| |
| def analyze_base_classes( |
| self, base_type_exprs: list[Expression] |
| ) -> tuple[list[tuple[ProperType, Expression]], bool] | None: |
| """Analyze base class types. |
| |
| Return None if some definition was incomplete. Otherwise, return a tuple |
| with these items: |
| |
| * List of (analyzed type, original expression) tuples |
| * Boolean indicating whether one of the bases had a semantic analysis error |
| """ |
| is_error = False |
| bases = [] |
| for base_expr in base_type_exprs: |
| if ( |
| isinstance(base_expr, RefExpr) |
| and base_expr.fullname in TYPED_NAMEDTUPLE_NAMES + TPDICT_NAMES |
| ): |
| # Ignore magic bases for now. |
| continue |
| |
| try: |
| base = self.expr_to_analyzed_type( |
| base_expr, allow_placeholder=True, allow_type_any=True |
| ) |
| except TypeTranslationError: |
| name = self.get_name_repr_of_expr(base_expr) |
| if isinstance(base_expr, CallExpr): |
| msg = "Unsupported dynamic base class" |
| else: |
| msg = "Invalid base class" |
| if name: |
| msg += f' "{name}"' |
| self.fail(msg, base_expr) |
| is_error = True |
| continue |
| if base is None: |
| return None |
| base = get_proper_type(base) |
| bases.append((base, base_expr)) |
| return bases, is_error |
| |
| def configure_base_classes( |
| self, defn: ClassDef, bases: list[tuple[ProperType, Expression]] |
| ) -> None: |
| """Set up base classes. |
| |
| This computes several attributes on the corresponding TypeInfo defn.info |
| related to the base classes: defn.info.bases, defn.info.mro, and |
| miscellaneous others (at least tuple_type, fallback_to_any, and is_enum.) |
| """ |
| base_types: list[Instance] = [] |
| info = defn.info |
| |
| for base, base_expr in bases: |
| if isinstance(base, TupleType): |
| actual_base = self.configure_tuple_base_class(defn, base) |
| base_types.append(actual_base) |
| elif isinstance(base, Instance): |
| if base.type.is_newtype: |
| self.fail('Cannot subclass "NewType"', defn) |
| base_types.append(base) |
| elif isinstance(base, AnyType): |
| if self.options.disallow_subclassing_any: |
| if isinstance(base_expr, (NameExpr, MemberExpr)): |
| msg = f'Class cannot subclass "{base_expr.name}" (has type "Any")' |
| else: |
| msg = 'Class cannot subclass value of type "Any"' |
| self.fail(msg, base_expr) |
| info.fallback_to_any = True |
| elif isinstance(base, TypedDictType): |
| base_types.append(base.fallback) |
| else: |
| msg = "Invalid base class" |
| name = self.get_name_repr_of_expr(base_expr) |
| if name: |
| msg += f' "{name}"' |
| self.fail(msg, base_expr) |
| info.fallback_to_any = True |
| if self.options.disallow_any_unimported and has_any_from_unimported_type(base): |
| if isinstance(base_expr, (NameExpr, MemberExpr)): |
| prefix = f"Base type {base_expr.name}" |
| else: |
| prefix = "Base type" |
| self.msg.unimported_type_becomes_any(prefix, base, base_expr) |
| check_for_explicit_any( |
| base, self.options, self.is_typeshed_stub_file, self.msg, context=base_expr |
| ) |
| |
| # Add 'object' as implicit base if there is no other base class. |
| if not base_types and defn.fullname != "builtins.object": |
| base_types.append(self.object_type()) |
| |
| info.bases = base_types |
| |
| # Calculate the MRO. |
| if not self.verify_base_classes(defn): |
| self.set_dummy_mro(defn.info) |
| return |
| if not self.verify_duplicate_base_classes(defn): |
| # We don't want to block the typechecking process, |
| # so, we just insert `Any` as the base class and show an error. |
| self.set_any_mro(defn.info) |
| self.calculate_class_mro(defn, self.object_type) |
| |
| def configure_tuple_base_class(self, defn: ClassDef, base: TupleType) -> Instance: |
| info = defn.info |
| |
| # There may be an existing valid tuple type from previous semanal iterations. |
| # Use equality to check if it is the case. |
| if info.tuple_type and info.tuple_type != base and not has_placeholder(info.tuple_type): |
| self.fail("Class has two incompatible bases derived from tuple", defn) |
| defn.has_incompatible_baseclass = True |
| if info.special_alias and has_placeholder(info.special_alias.target): |
| self.process_placeholder( |
| None, "tuple base", defn, force_progress=base != info.tuple_type |
| ) |
| info.update_tuple_type(base) |
| self.setup_alias_type_vars(defn) |
| |
| if base.partial_fallback.type.fullname == "builtins.tuple" and not has_placeholder(base): |
| # Fallback can only be safely calculated after semantic analysis, since base |
| # classes may be incomplete. Postpone the calculation. |
| self.schedule_patch(PRIORITY_FALLBACKS, lambda: calculate_tuple_fallback(base)) |
| |
| return base.partial_fallback |
| |
| def set_dummy_mro(self, info: TypeInfo) -> None: |
| # Give it an MRO consisting of just the class itself and object. |
| info.mro = [info, self.object_type().type] |
| info.bad_mro = True |
| |
| def set_any_mro(self, info: TypeInfo) -> None: |
| # Give it an MRO consisting direct `Any` subclass. |
| info.fallback_to_any = True |
| info.mro = [info, self.object_type().type] |
| |
| def calculate_class_mro( |
| self, defn: ClassDef, obj_type: Callable[[], Instance] | None = None |
| ) -> None: |
| """Calculate method resolution order for a class. |
| |
| `obj_type` exists just to fill in empty base class list in case of an error. |
| """ |
| try: |
| calculate_mro(defn.info, obj_type) |
| except MroError: |
| self.fail( |
| "Cannot determine consistent method resolution " |
| 'order (MRO) for "%s"' % defn.name, |
| defn, |
| ) |
| self.set_dummy_mro(defn.info) |
| # Allow plugins to alter the MRO to handle the fact that `def mro()` |
| # on metaclasses permits MRO rewriting. |
| if defn.fullname: |
| hook = self.plugin.get_customize_class_mro_hook(defn.fullname) |
| if hook: |
| hook(ClassDefContext(defn, FakeExpression(), self)) |
| |
| def infer_metaclass_and_bases_from_compat_helpers(self, defn: ClassDef) -> None: |
| """Lookup for special metaclass declarations, and update defn fields accordingly. |
| |
| * six.with_metaclass(M, B1, B2, ...) |
| * @six.add_metaclass(M) |
| * future.utils.with_metaclass(M, B1, B2, ...) |
| * past.utils.with_metaclass(M, B1, B2, ...) |
| """ |
| |
| # Look for six.with_metaclass(M, B1, B2, ...) |
| with_meta_expr: Expression | None = None |
| if len(defn.base_type_exprs) == 1: |
| base_expr = defn.base_type_exprs[0] |
| if isinstance(base_expr, CallExpr) and isinstance(base_expr.callee, RefExpr): |
| self.analyze_type_expr(base_expr) |
| if ( |
| base_expr.callee.fullname |
| in { |
| "six.with_metaclass", |
| "future.utils.with_metaclass", |
| "past.utils.with_metaclass", |
| } |
| and len(base_expr.args) >= 1 |
| and all(kind == ARG_POS for kind in base_expr.arg_kinds) |
| ): |
| with_meta_expr = base_expr.args[0] |
| defn.base_type_exprs = base_expr.args[1:] |
| |
| # Look for @six.add_metaclass(M) |
| add_meta_expr: Expression | None = None |
| for dec_expr in defn.decorators: |
| if isinstance(dec_expr, CallExpr) and isinstance(dec_expr.callee, RefExpr): |
| dec_expr.callee.accept(self) |
| if ( |
| dec_expr.callee.fullname == "six.add_metaclass" |
| and len(dec_expr.args) == 1 |
| and dec_expr.arg_kinds[0] == ARG_POS |
| ): |
| add_meta_expr = dec_expr.args[0] |
| break |
| |
| metas = {defn.metaclass, with_meta_expr, add_meta_expr} - {None} |
| if len(metas) == 0: |
| return |
| if len(metas) > 1: |
| self.fail("Multiple metaclass definitions", defn) |
| return |
| defn.metaclass = metas.pop() |
| |
| def verify_base_classes(self, defn: ClassDef) -> bool: |
| info = defn.info |
| cycle = False |
| for base in info.bases: |
| baseinfo = base.type |
| if self.is_base_class(info, baseinfo): |
| self.fail("Cycle in inheritance hierarchy", defn) |
| cycle = True |
| return not cycle |
| |
| def verify_duplicate_base_classes(self, defn: ClassDef) -> bool: |
| dup = find_duplicate(defn.info.direct_base_classes()) |
| if dup: |
| self.fail(f'Duplicate base class "{dup.name}"', defn) |
| return not dup |
| |
| def is_base_class(self, t: TypeInfo, s: TypeInfo) -> bool: |
| """Determine if t is a base class of s (but do not use mro).""" |
| # Search the base class graph for t, starting from s. |
| worklist = [s] |
| visited = {s} |
| while worklist: |
| nxt = worklist.pop() |
| if nxt == t: |
| return True |
| for base in nxt.bases: |
| if base.type not in visited: |
| worklist.append(base.type) |
| visited.add(base.type) |
| return False |
| |
| def get_declared_metaclass( |
| self, name: str, metaclass_expr: Expression | None |
| ) -> tuple[Instance | None, bool, bool]: |
| """Get declared metaclass from metaclass expression. |
| |
| Returns a tuple of three values: |
| * A metaclass instance or None |
| * A boolean indicating whether we should defer |
| * A boolean indicating whether we should set metaclass Any fallback |
| (either for Any metaclass or invalid/dynamic metaclass). |
| |
| The two boolean flags can only be True if instance is None. |
| """ |
| declared_metaclass = None |
| if metaclass_expr: |
| metaclass_name = None |
| if isinstance(metaclass_expr, NameExpr): |
| metaclass_name = metaclass_expr.name |
| elif isinstance(metaclass_expr, MemberExpr): |
| metaclass_name = get_member_expr_fullname(metaclass_expr) |
| if metaclass_name is None: |
| self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr) |
| return None, False, True |
| sym = self.lookup_qualified(metaclass_name, metaclass_expr) |
| if sym is None: |
| # Probably a name error - it is already handled elsewhere |
| return None, False, True |
| if isinstance(sym.node, Var) and isinstance(get_proper_type(sym.node.type), AnyType): |
| if self.options.disallow_subclassing_any: |
| self.fail( |
| f'Class cannot use "{sym.node.name}" as a metaclass (has type "Any")', |
| metaclass_expr, |
| ) |
| return None, False, True |
| if isinstance(sym.node, PlaceholderNode): |
| return None, True, False # defer later in the caller |
| |
| # Support type aliases, like `_Meta: TypeAlias = type` |
| if ( |
| isinstance(sym.node, TypeAlias) |
| and sym.node.no_args |
| and isinstance(sym.node.target, ProperType) |
| and isinstance(sym.node.target, Instance) |
| ): |
| metaclass_info: Node | None = sym.node.target.type |
| else: |
| metaclass_info = sym.node |
| |
| if not isinstance(metaclass_info, TypeInfo) or metaclass_info.tuple_type is not None: |
| self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr) |
| return None, False, False |
| if not metaclass_info.is_metaclass(): |
| self.fail( |
| 'Metaclasses not inheriting from "type" are not supported', metaclass_expr |
| ) |
| return None, False, False |
| inst = fill_typevars(metaclass_info) |
| assert isinstance(inst, Instance) |
| declared_metaclass = inst |
| return declared_metaclass, False, False |
| |
| def recalculate_metaclass(self, defn: ClassDef, declared_metaclass: Instance | None) -> None: |
| defn.info.declared_metaclass = declared_metaclass |
| defn.info.metaclass_type = defn.info.calculate_metaclass_type() |
| if any(info.is_protocol for info in defn.info.mro): |
| if ( |
| not defn.info.metaclass_type |
| or defn.info.metaclass_type.type.fullname == "builtins.type" |
| ): |
| # All protocols and their subclasses have ABCMeta metaclass by default. |
| # TODO: add a metaclass conflict check if there is another metaclass. |
| abc_meta = self.named_type_or_none("abc.ABCMeta", []) |
| if abc_meta is not None: # May be None in tests with incomplete lib-stub. |
| defn.info.metaclass_type = abc_meta |
| if defn.info.metaclass_type and defn.info.metaclass_type.type.has_base("enum.EnumMeta"): |
| defn.info.is_enum = True |
| if defn.type_vars: |
| self.fail("Enum class cannot be generic", defn) |
| |
| # |
| # Imports |
| # |
| |
| def visit_import(self, i: Import) -> None: |
| self.statement = i |
| for id, as_id in i.ids: |
| # Modules imported in a stub file without using 'import X as X' won't get exported |
| # When implicit re-exporting is disabled, we have the same behavior as stubs. |
| use_implicit_reexport = not self.is_stub_file and self.options.implicit_reexport |
| if as_id is not None: |
| base_id = id |
| imported_id = as_id |
| module_public = use_implicit_reexport or id.split(".")[-1] == as_id |
| else: |
| base_id = id.split(".")[0] |
| imported_id = base_id |
| module_public = use_implicit_reexport |
| |
| if base_id in self.modules: |
| node = self.modules[base_id] |
| if self.is_func_scope(): |
| kind = LDEF |
| elif self.type is not None: |
| kind = MDEF |
| else: |
| kind = GDEF |
| symbol = SymbolTableNode( |
| kind, node, module_public=module_public, module_hidden=not module_public |
| ) |
| self.add_imported_symbol( |
| imported_id, |
| symbol, |
| context=i, |
| module_public=module_public, |
| module_hidden=not module_public, |
| ) |
| else: |
| self.add_unknown_imported_symbol( |
| imported_id, |
| context=i, |
| target_name=base_id, |
| module_public=module_public, |
| module_hidden=not module_public, |
| ) |
| |
| def visit_import_from(self, imp: ImportFrom) -> None: |
| self.statement = imp |
| module_id = self.correct_relative_import(imp) |
| module = self.modules.get(module_id) |
| for id, as_id in imp.names: |
| fullname = module_id + "." + id |
| self.set_future_import_flags(fullname) |
| if module is None: |
| node = None |
| elif module_id == self.cur_mod_id and fullname in self.modules: |
| # Submodule takes precedence over definition in surround package, for |
| # compatibility with runtime semantics in typical use cases. This |
| # could more precisely model runtime semantics by taking into account |
| # the line number beyond which the local definition should take |
| # precedence, but doesn't seem to be important in most use cases. |
| node = SymbolTableNode(GDEF, self.modules[fullname]) |
| else: |
| if id == as_id == "__all__" and module_id in self.export_map: |
| self.all_exports[:] = self.export_map[module_id] |
| node = module.names.get(id) |
| |
| missing_submodule = False |
| imported_id = as_id or id |
| |
| # Modules imported in a stub file without using 'from Y import X as X' will |
| # not get exported. |
| # When implicit re-exporting is disabled, we have the same behavior as stubs. |
| use_implicit_reexport = not self.is_stub_file and self.options.implicit_reexport |
| module_public = use_implicit_reexport or (as_id is not None and id == as_id) |
| |
| # If the module does not contain a symbol with the name 'id', |
| # try checking if it's a module instead. |
| if not node: |
| mod = self.modules.get(fullname) |
| if mod is not None: |
| kind = self.current_symbol_kind() |
| node = SymbolTableNode(kind, mod) |
| elif fullname in self.missing_modules: |
| missing_submodule = True |
| # If it is still not resolved, check for a module level __getattr__ |
| if module and not node and "__getattr__" in module.names: |
| # We store the fullname of the original definition so that we can |
| # detect whether two imported names refer to the same thing. |
| fullname = module_id + "." + id |
| gvar = self.create_getattr_var(module.names["__getattr__"], imported_id, fullname) |
| if gvar: |
| self.add_symbol( |
| imported_id, |
| gvar, |
| imp, |
| module_public=module_public, |
| module_hidden=not module_public, |
| ) |
| continue |
| |
| if node: |
| self.process_imported_symbol( |
| node, module_id, id, imported_id, fullname, module_public, context=imp |
| ) |
| if node.module_hidden: |
| self.report_missing_module_attribute( |
| module_id, |
| id, |
| imported_id, |
| module_public=module_public, |
| module_hidden=not module_public, |
| context=imp, |
| add_unknown_imported_symbol=False, |
| ) |
| elif module and not missing_submodule: |
| # Target module exists but the imported name is missing or hidden. |
| self.report_missing_module_attribute( |
| module_id, |
| id, |
| imported_id, |
| module_public=module_public, |
| module_hidden=not module_public, |
| context=imp, |
| ) |
| else: |
| # Import of a missing (sub)module. |
| self.add_unknown_imported_symbol( |
| imported_id, |
| imp, |
| target_name=fullname, |
| module_public=module_public, |
| module_hidden=not module_public, |
| ) |
| |
| def process_imported_symbol( |
| self, |
| node: SymbolTableNode, |
| module_id: str, |
| id: str, |
| imported_id: str, |
| fullname: str, |
| module_public: bool, |
| context: ImportBase, |
| ) -> None: |
| module_hidden = not module_public and ( |
| # `from package import submodule` should work regardless of whether package |
| # re-exports submodule, so we shouldn't hide it |
| not isinstance(node.node, MypyFile) |
| or fullname not in self.modules |
| # but given `from somewhere import random_unrelated_module` we should hide |
| # random_unrelated_module |
| or not fullname.startswith(self.cur_mod_id + ".") |
| ) |
| |
| if isinstance(node.node, PlaceholderNode): |
| if self.final_iteration: |
| self.report_missing_module_attribute( |
| module_id, |
| id, |
| imported_id, |
| module_public=module_public, |
| module_hidden=module_hidden, |
| context=context, |
| ) |
| return |
| else: |
| # This might become a type. |
| self.mark_incomplete( |
| imported_id, |
| node.node, |
| module_public=module_public, |
| module_hidden=module_hidden, |
| becomes_typeinfo=True, |
| ) |
| # NOTE: we take the original node even for final `Var`s. This is to support |
| # a common pattern when constants are re-exported (same applies to import *). |
| self.add_imported_symbol( |
| imported_id, node, context, module_public=module_public, module_hidden=module_hidden |
| ) |
| |
| def report_missing_module_attribute( |
| self, |
| import_id: str, |
| source_id: str, |
| imported_id: str, |
| module_public: bool, |
| module_hidden: bool, |
| context: Node, |
| add_unknown_imported_symbol: bool = True, |
| ) -> None: |
| # Missing attribute. |
| if self.is_incomplete_namespace(import_id): |
| # We don't know whether the name will be there, since the namespace |
| # is incomplete. Defer the current target. |
| self.mark_incomplete( |
| imported_id, context, module_public=module_public, module_hidden=module_hidden |
| ) |
| return |
| message = f'Module "{import_id}" has no attribute "{source_id}"' |
| # Suggest alternatives, if any match is found. |
| module = self.modules.get(import_id) |
| if module: |
| if source_id in module.names.keys() and not module.names[source_id].module_public: |
| message = ( |
| f'Module "{import_id}" does not explicitly export attribute "{source_id}"' |
| ) |
| else: |
| alternatives = set(module.names.keys()).difference({source_id}) |
| matches = best_matches(source_id, alternatives, n=3) |
| if matches: |
| suggestion = f"; maybe {pretty_seq(matches, 'or')}?" |
| message += f"{suggestion}" |
| self.fail(message, context, code=codes.ATTR_DEFINED) |
| if add_unknown_imported_symbol: |
| self.add_unknown_imported_symbol( |
| imported_id, |
| context, |
| target_name=None, |
| module_public=module_public, |
| module_hidden=not module_public, |
| ) |
| |
| if import_id == "typing": |
| # The user probably has a missing definition in a test fixture. Let's verify. |
| fullname = f"builtins.{source_id.lower()}" |
| if ( |
| self.lookup_fully_qualified_or_none(fullname) is None |
| and fullname in SUGGESTED_TEST_FIXTURES |
| ): |
| # Yes. Generate a helpful note. |
| self.msg.add_fixture_note(fullname, context) |
| else: |
| typing_extensions = self.modules.get("typing_extensions") |
| if typing_extensions and source_id in typing_extensions.names: |
| self.msg.note( |
| f"Use `from typing_extensions import {source_id}` instead", |
| context, |
| code=codes.ATTR_DEFINED, |
| ) |
| self.msg.note( |
| "See https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-new-additions-to-the-typing-module", |
| context, |
| code=codes.ATTR_DEFINED, |
| ) |
| |
| def process_import_over_existing_name( |
| self, |
| imported_id: str, |
| existing_symbol: SymbolTableNode, |
| module_symbol: SymbolTableNode, |
| import_node: ImportBase, |
| ) -> bool: |
| if existing_symbol.node is module_symbol.node: |
| # We added this symbol on previous iteration. |
| return False |
| if existing_symbol.kind in (LDEF, GDEF, MDEF) and isinstance( |
| existing_symbol.node, (Var, FuncDef, TypeInfo, Decorator, TypeAlias) |
| ): |
| # This is a valid import over an existing definition in the file. Construct a dummy |
| # assignment that we'll use to type check the import. |
| lvalue = NameExpr(imported_id) |
| lvalue.kind = existing_symbol.kind |
| lvalue.node = existing_symbol.node |
| rvalue = NameExpr(imported_id) |
| rvalue.kind = module_symbol.kind |
| rvalue.node = module_symbol.node |
| if isinstance(rvalue.node, TypeAlias): |
| # Suppress bogus errors from the dummy assignment if rvalue is an alias. |
| # Otherwise mypy may complain that alias is invalid in runtime context. |
| rvalue.is_alias_rvalue = True |
| assignment = AssignmentStmt([lvalue], rvalue) |
| for node in assignment, lvalue, rvalue: |
| node.set_line(import_node) |
| import_node.assignments.append(assignment) |
| return True |
| return False |
| |
| def correct_relative_import(self, node: ImportFrom | ImportAll) -> str: |
| import_id, ok = correct_relative_import( |
| self.cur_mod_id, node.relative, node.id, self.cur_mod_node.is_package_init_file() |
| ) |
| if not ok: |
| self.fail("Relative import climbs too many namespaces", node) |
| return import_id |
| |
| def visit_import_all(self, i: ImportAll) -> None: |
| i_id = self.correct_relative_import(i) |
| if i_id in self.modules: |
| m = self.modules[i_id] |
| if self.is_incomplete_namespace(i_id): |
| # Any names could be missing from the current namespace if the target module |
| # namespace is incomplete. |
| self.mark_incomplete("*", i) |
| for name, node in m.names.items(): |
| fullname = i_id + "." + name |
| self.set_future_import_flags(fullname) |
| if node is None: |
| continue |
| # if '__all__' exists, all nodes not included have had module_public set to |
| # False, and we can skip checking '_' because it's been explicitly included. |
| if node.module_public and (not name.startswith("_") or "__all__" in m.names): |
| if isinstance(node.node, MypyFile): |
| # Star import of submodule from a package, add it as a dependency. |
| self.imports.add(node.node.fullname) |
| # `from x import *` always reexports symbols |
| self.add_imported_symbol( |
| name, node, context=i, module_public=True, module_hidden=False |
| ) |
| |
| else: |
| # Don't add any dummy symbols for 'from x import *' if 'x' is unknown. |
| pass |
| |
| # |
| # Assignment |
| # |
| |
| def visit_assignment_expr(self, s: AssignmentExpr) -> None: |
| s.value.accept(self) |
| if self.is_func_scope(): |
| if not self.check_valid_comprehension(s): |
| return |
| self.analyze_lvalue(s.target, escape_comprehensions=True, has_explicit_value=True) |
| |
| def check_valid_comprehension(self, s: AssignmentExpr) -> bool: |
| """Check that assignment expression is not nested within comprehension at class scope. |
| |
| class C: |
| [(j := i) for i in [1, 2, 3]] |
| is a syntax error that is not enforced by Python parser, but at later steps. |
| """ |
| for i, is_comprehension in enumerate(reversed(self.is_comprehension_stack)): |
| if not is_comprehension and i < len(self.locals) - 1: |
| if self.locals[-1 - i] is None: |
| self.fail( |
| "Assignment expression within a comprehension" |
| " cannot be used in a class body", |
| s, |
| code=codes.SYNTAX, |
| serious=True, |
| blocker=True, |
| ) |
| return False |
| break |
| return True |
| |
| def visit_assignment_stmt(self, s: AssignmentStmt) -> None: |
| self.statement = s |
| |
| # Special case assignment like X = X. |
| if self.analyze_identity_global_assignment(s): |
| return |
| |
| tag = self.track_incomplete_refs() |
| |
| # Here we have a chicken and egg problem: at this stage we can't call |
| # can_be_type_alias(), because we have not enough information about rvalue. |
| # But we can't use a full visit because it may emit extra incomplete refs (namely |
| # when analysing any type applications there) thus preventing the further analysis. |
| # To break the tie, we first analyse rvalue partially, if it can be a type alias. |
| if self.can_possibly_be_type_form(s): |
| old_basic_type_applications = self.basic_type_applications |
| self.basic_type_applications = True |
| with self.allow_unbound_tvars_set(): |
| s.rvalue.accept(self) |
| self.basic_type_applications = old_basic_type_applications |
| else: |
| s.rvalue.accept(self) |
| |
| if self.found_incomplete_ref(tag) or self.should_wait_rhs(s.rvalue): |
| # Initializer couldn't be fully analyzed. Defer the current node and give up. |
| # Make sure that if we skip the definition of some local names, they can't be |
| # added later in this scope, since an earlier definition should take precedence. |
| for expr in names_modified_by_assignment(s): |
| self.mark_incomplete(expr.name, expr) |
| return |
| if self.can_possibly_be_type_form(s): |
| # Now re-visit those rvalues that were we skipped type applications above. |
| # This should be safe as generally semantic analyzer is idempotent. |
| with self.allow_unbound_tvars_set(): |
| s.rvalue.accept(self) |
| |
| # The r.h.s. is now ready to be classified, first check if it is a special form: |
| special_form = False |
| # * type alias |
| if self.check_and_set_up_type_alias(s): |
| s.is_alias_def = True |
| special_form = True |
| # * type variable definition |
| elif self.process_typevar_declaration(s): |
| special_form = True |
| elif self.process_paramspec_declaration(s): |
| special_form = True |
| elif self.process_typevartuple_declaration(s): |
| special_form = True |
| # * type constructors |
| elif self.analyze_namedtuple_assign(s): |
| special_form = True |
| elif self.analyze_typeddict_assign(s): |
| special_form = True |
| elif self.newtype_analyzer.process_newtype_declaration(s): |
| special_form = True |
| elif self.analyze_enum_assign(s): |
| special_form = True |
| |
| if special_form: |
| self.record_special_form_lvalue(s) |
| return |
| # Clear the alias flag if assignment turns out not a special form after all. It |
| # may be set to True while there were still placeholders due to forward refs. |
| s.is_alias_def = False |
| |
| # OK, this is a regular assignment, perform the necessary analysis steps. |
| s.is_final_def = self.unwrap_final(s) |
| self.analyze_lvalues(s) |
| self.check_final_implicit_def(s) |
| self.store_final_status(s) |
| self.check_classvar(s) |
| self.process_type_annotation(s) |
| self.apply_dynamic_class_hook(s) |
| if not s.type: |
| self.process_module_assignment(s.lvalues, s.rvalue, s) |
| self.process__all__(s) |
| self.process__deletable__(s) |
| self.process__slots__(s) |
| |
| def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool: |
| """Special case 'X = X' in global scope. |
| |
| This allows supporting some important use cases. |
| |
| Return true if special casing was applied. |
| """ |
| if not isinstance(s.rvalue, NameExpr) or len(s.lvalues) != 1: |
| # Not of form 'X = X' |
| return False |
| lvalue = s.lvalues[0] |
| if not isinstance(lvalue, NameExpr) or s.rvalue.name != lvalue.name: |
| # Not of form 'X = X' |
| return False |
| if self.type is not None or self.is_func_scope(): |
| # Not in global scope |
| return False |
| # It's an assignment like 'X = X' in the global scope. |
| name = lvalue.name |
| sym = self.lookup(name, s) |
| if sym is None: |
| if self.final_iteration: |
| # Fall back to normal assignment analysis. |
| return False |
| else: |
| self.defer() |
| return True |
| else: |
| if sym.node is None: |
| # Something special -- fall back to normal assignment analysis. |
| return False |
| if name not in self.globals: |
| # The name is from builtins. Add an alias to the current module. |
| self.add_symbol(name, sym.node, s) |
| if not isinstance(sym.node, PlaceholderNode): |
| for node in s.rvalue, lvalue: |
| node.node = sym.node |
| node.kind = GDEF |
| node.fullname = sym.node.fullname |
| return True |
| |
| def should_wait_rhs(self, rv: Expression) -> bool: |
| """Can we already classify this r.h.s. of an assignment or should we wait? |
| |
| This returns True if we don't have enough information to decide whether |
| an assignment is just a normal variable definition or a special form. |
| Always return False if this is a final iteration. This will typically cause |
| the lvalue to be classified as a variable plus emit an error. |
| """ |
| if self.final_iteration: |
| # No chance, nothing has changed. |
| return False |
| if isinstance(rv, NameExpr): |
| n = self.lookup(rv.name, rv) |
| if n and isinstance(n.node, PlaceholderNode) and not n.node.becomes_typeinfo: |
| return True |
| elif isinstance(rv, MemberExpr): |
| fname = get_member_expr_fullname(rv) |
| if fname: |
| n = self.lookup_qualified(fname, rv, suppress_errors=True) |
| if n and isinstance(n.node, PlaceholderNode) and not n.node.becomes_typeinfo: |
| return True |
| elif isinstance(rv, IndexExpr) and isinstance(rv.base, RefExpr): |
| return self.should_wait_rhs(rv.base) |
| elif isinstance(rv, CallExpr) and isinstance(rv.callee, RefExpr): |
| # This is only relevant for builtin SCC where things like 'TypeVar' |
| # may be not ready. |
| return self.should_wait_rhs(rv.callee) |
| return False |
| |
| def can_be_type_alias(self, rv: Expression, allow_none: bool = False) -> bool: |
| """Is this a valid r.h.s. for an alias definition? |
| |
| Note: this function should be only called for expressions where self.should_wait_rhs() |
| returns False. |
| """ |
| if isinstance(rv, RefExpr) and self.is_type_ref(rv, bare=True): |
| return True |
| if isinstance(rv, IndexExpr) and self.is_type_ref(rv.base, bare=False): |
| return True |
| if self.is_none_alias(rv): |
| return True |
| if allow_none and isinstance(rv, NameExpr) and rv.fullname == "builtins.None": |
| return True |
| if isinstance(rv, OpExpr) and rv.op == "|": |
| if self.is_stub_file: |
| return True |
| if self.can_be_type_alias(rv.left, allow_none=True) and self.can_be_type_alias( |
| rv.right, allow_none=True |
| ): |
| return True |
| return False |
| |
| def can_possibly_be_type_form(self, s: AssignmentStmt) -> bool: |
| """Like can_be_type_alias(), but simpler and doesn't require fully analyzed rvalue. |
| |
| Instead, use lvalues/annotations structure to figure out whether this can potentially be |
| a type alias definition, NamedTuple, or TypedDict. Another difference from above function |
| is that we are only interested IndexExpr, CallExpr and OpExpr rvalues, since only those |
| can be potentially recursive (things like `A = A` are never valid). |
| """ |
| if len(s.lvalues) > 1: |
| return False |
| if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.callee, RefExpr): |
| ref = s.rvalue.callee.fullname |
| return ref in TPDICT_NAMES or ref in TYPED_NAMEDTUPLE_NAMES |
| if not isinstance(s.lvalues[0], NameExpr): |
| return False |
| if s.unanalyzed_type is not None and not self.is_pep_613(s): |
| return False |
| if not isinstance(s.rvalue, (IndexExpr, OpExpr)): |
| return False |
| # Something that looks like Foo = Bar[Baz, ...] |
| return True |
| |
| def is_type_ref(self, rv: Expression, bare: bool = False) -> bool: |
| """Does this expression refer to a type? |
| |
| This includes: |
| * Special forms, like Any or Union |
| * Classes (except subscripted enums) |
| * Other type aliases |
| * PlaceholderNodes with becomes_typeinfo=True (these can be not ready class |
| definitions, and not ready aliases). |
| |
| If bare is True, this is not a base of an index expression, so some special |
| forms are not valid (like a bare Union). |
| |
| Note: This method should be only used in context of a type alias definition. |
| This method can only return True for RefExprs, to check if C[int] is a valid |
| target for type alias call this method on expr.base (i.e. on C in C[int]). |
| See also can_be_type_alias(). |
| """ |
| if not isinstance(rv, RefExpr): |
| return False |
| if isinstance(rv.node, TypeVarLikeExpr): |
| self.fail(f'Type variable "{rv.fullname}" is invalid as target for type alias', rv) |
| return False |
| |
| if bare: |
| # These three are valid even if bare, for example |
| # A = Tuple is just equivalent to A = Tuple[Any, ...]. |
| valid_refs = {"typing.Any", "typing.Tuple", "typing.Callable"} |
| else: |
| valid_refs = type_constructors |
| |
| if isinstance(rv.node, TypeAlias) or rv.fullname in valid_refs: |
| return True |
| if isinstance(rv.node, TypeInfo): |
| if bare: |
| return True |
| # Assignment color = Color['RED'] defines a variable, not an alias. |
| return not rv.node.is_enum |
| if isinstance(rv.node, Var): |
| return rv.node.fullname in NEVER_NAMES |
| |
| if isinstance(rv, NameExpr): |
| n = self.lookup(rv.name, rv) |
| if n and isinstance(n.node, PlaceholderNode) and n.node.becomes_typeinfo: |
| return True |
| elif isinstance(rv, MemberExpr): |
| fname = get_member_expr_fullname(rv) |
| if fname: |
| # The r.h.s. for variable definitions may not be a type reference but just |
| # an instance attribute, so suppress the errors. |
| n = self.lookup_qualified(fname, rv, suppress_errors=True) |
| if n and isinstance(n.node, PlaceholderNode) and n.node.becomes_typeinfo: |
| return True |
| return False |
| |
| def is_none_alias(self, node: Expression) -> bool: |
| """Is this a r.h.s. for a None alias? |
| |
| We special case the assignments like Void = type(None), to allow using |
| Void in type annotations. |
| """ |
| if isinstance(node, CallExpr): |
| if ( |
| isinstance(node.callee, NameExpr) |
| and len(node.args) == 1 |
| and isinstance(node.args[0], NameExpr) |
| ): |
| call = self.lookup_qualified(node.callee.name, node.callee) |
| arg = self.lookup_qualified(node.args[0].name, node.args[0]) |
| if ( |
| call is not None |
| and call.node |
| and call.node.fullname == "builtins.type" |
| and arg is not None |
| and arg.node |
| and arg.node.fullname == "builtins.None" |
| ): |
| return True |
| return False |
| |
| def record_special_form_lvalue(self, s: AssignmentStmt) -> None: |
| """Record minimal necessary information about l.h.s. of a special form. |
| |
| This exists mostly for compatibility with the old semantic analyzer. |
| """ |
| lvalue = s.lvalues[0] |
| assert isinstance(lvalue, NameExpr) |
| lvalue.is_special_form = True |
| if self.current_symbol_kind() == GDEF: |
| lvalue.fullname = self.qualified_name(lvalue.name) |
| lvalue.kind = self.current_symbol_kind() |
| |
| def analyze_enum_assign(self, s: AssignmentStmt) -> bool: |
| """Check if s defines an Enum.""" |
| if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.analyzed, EnumCallExpr): |
| # Already analyzed enum -- nothing to do here. |
| return True |
| return self.enum_call_analyzer.process_enum_call(s, self.is_func_scope()) |
| |
| def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool: |
| """Check if s defines a namedtuple.""" |
| if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.analyzed, NamedTupleExpr): |
| if s.rvalue.analyzed.info.tuple_type and not has_placeholder( |
| s.rvalue.analyzed.info.tuple_type |
| ): |
| return True # This is a valid and analyzed named tuple definition, nothing to do here. |
| if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], (NameExpr, MemberExpr)): |
| return False |
| lvalue = s.lvalues[0] |
| if isinstance(lvalue, MemberExpr): |
| if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.callee, RefExpr): |
| fullname = s.rvalue.callee.fullname |
| if fullname == "collections.namedtuple" or fullname in TYPED_NAMEDTUPLE_NAMES: |
| self.fail("NamedTuple type as an attribute is not supported", lvalue) |
| return False |
| name = lvalue.name |
| namespace = self.qualified_name(name) |
| with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): |
| internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple( |
| s.rvalue, name, self.is_func_scope() |
| ) |
| if internal_name is None: |
| return False |
| if internal_name != name: |
| self.fail( |
| 'First argument to namedtuple() should be "{}", not "{}"'.format( |
| name, internal_name |
| ), |
| s.rvalue, |
| code=codes.NAME_MATCH, |
| ) |
| return True |
| # Yes, it's a valid namedtuple, but defer if it is not ready. |
| if not info: |
| self.mark_incomplete(name, lvalue, becomes_typeinfo=True) |
| else: |
| self.setup_type_vars(info.defn, tvar_defs) |
| self.setup_alias_type_vars(info.defn) |
| return True |
| |
| def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool: |
| """Check if s defines a typed dict.""" |
| if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.analyzed, TypedDictExpr): |
| if s.rvalue.analyzed.info.typeddict_type and not has_placeholder( |
| s.rvalue.analyzed.info.typeddict_type |
| ): |
| # This is a valid and analyzed typed dict definition, nothing to do here. |
| return True |
| if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], (NameExpr, MemberExpr)): |
| return False |
| lvalue = s.lvalues[0] |
| name = lvalue.name |
| namespace = self.qualified_name(name) |
| with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): |
| is_typed_dict, info, tvar_defs = self.typed_dict_analyzer.check_typeddict( |
| s.rvalue, name, self.is_func_scope() |
| ) |
| if not is_typed_dict: |
| return False |
| if isinstance(lvalue, MemberExpr): |
| self.fail("TypedDict type as attribute is not supported", lvalue) |
| return False |
| # Yes, it's a valid typed dict, but defer if it is not ready. |
| if not info: |
| self.mark_incomplete(name, lvalue, becomes_typeinfo=True) |
| else: |
| defn = info.defn |
| self.setup_type_vars(defn, tvar_defs) |
| self.setup_alias_type_vars(defn) |
| return True |
| |
| def analyze_lvalues(self, s: AssignmentStmt) -> None: |
| # We cannot use s.type, because analyze_simple_literal_type() will set it. |
| explicit = s.unanalyzed_type is not None |
| if self.is_final_type(s.unanalyzed_type): |
| # We need to exclude bare Final. |
| assert isinstance(s.unanalyzed_type, UnboundType) |
| if not s.unanalyzed_type.args: |
| explicit = False |
| |
| if s.rvalue: |
| if isinstance(s.rvalue, TempNode): |
| has_explicit_value = not s.rvalue.no_rhs |
| else: |
| has_explicit_value = True |
| else: |
| has_explicit_value = False |
| |
| for lval in s.lvalues: |
| self.analyze_lvalue( |
| lval, |
| explicit_type=explicit, |
| is_final=s.is_final_def, |
| has_explicit_value=has_explicit_value, |
| ) |
| |
| def apply_dynamic_class_hook(self, s: AssignmentStmt) -> None: |
| if not isinstance(s.rvalue, CallExpr): |
| return |
| fname = "" |
| call = s.rvalue |
| while True: |
| if isinstance(call.callee, RefExpr): |
| fname = call.callee.fullname |
| # check if method call |
| if not fname and isinstance(call.callee, MemberExpr): |
| callee_expr = call.callee.expr |
| if isinstance(callee_expr, RefExpr) and callee_expr.fullname: |
| method_name = call.callee.name |
| fname = callee_expr.fullname + "." + method_name |
| elif isinstance(callee_expr, CallExpr): |
| # check if chain call |
| call = callee_expr |
| continue |
| break |
| if not fname: |
| return |
| hook = self.plugin.get_dynamic_class_hook(fname) |
| if not hook: |
| return |
| for lval in s.lvalues: |
| if not isinstance(lval, NameExpr): |
| continue |
| hook(DynamicClassDefContext(call, lval.name, self)) |
| |
| def unwrap_final(self, s: AssignmentStmt) -> bool: |
| """Strip Final[...] if present in an assignment. |
| |
| This is done to invoke type inference during type checking phase for this |
| assignment. Also, Final[...] doesn't affect type in any way -- it is rather an |
| access qualifier for given `Var`. |
| |
| Also perform various consistency checks. |
| |
| Returns True if Final[...] was present. |
| """ |
| if not s.unanalyzed_type or not self.is_final_type(s.unanalyzed_type): |
| return False |
| assert isinstance(s.unanalyzed_type, UnboundType) |
| if len(s.unanalyzed_type.args) > 1: |
| self.fail("Final[...] takes at most one type argument", s.unanalyzed_type) |
| invalid_bare_final = False |
| if not s.unanalyzed_type.args: |
| s.type = None |
| if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs: |
| invalid_bare_final = True |
| self.fail("Type in Final[...] can only be omitted if there is an initializer", s) |
| else: |
| s.type = s.unanalyzed_type.args[0] |
| |
| if s.type is not None and self.is_classvar(s.type): |
| self.fail("Variable should not be annotated with both ClassVar and Final", s) |
| return False |
| |
| if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], RefExpr): |
| self.fail("Invalid final declaration", s) |
| return False |
| lval = s.lvalues[0] |
| assert isinstance(lval, RefExpr) |
| |
| # Reset inferred status if it was set due to simple literal rvalue on previous iteration. |
| # TODO: this is a best-effort quick fix, we should avoid the need to manually sync this, |
| # see https://github.com/python/mypy/issues/6458. |
| if lval.is_new_def: |
| lval.is_inferred_def = s.type is None |
| |
| if self.loop_depth[-1] > 0: |
| self.fail("Cannot use Final inside a loop", s) |
| if self.type and self.type.is_protocol: |
| self.msg.protocol_members_cant_be_final(s) |
| if ( |
| isinstance(s.rvalue, TempNode) |
| and s.rvalue.no_rhs |
| and not self.is_stub_file |
| and not self.is_class_scope() |
| ): |
| if not invalid_bare_final: # Skip extra error messages. |
| self.msg.final_without_value(s) |
| return True |
| |
| def check_final_implicit_def(self, s: AssignmentStmt) -> None: |
| """Do basic checks for final declaration on self in __init__. |
| |
| Additional re-definition checks are performed by `analyze_lvalue`. |
| """ |
| if not s.is_final_def: |
| return |
| lval = s.lvalues[0] |
| assert isinstance(lval, RefExpr) |
| if isinstance(lval, MemberExpr): |
| if not self.is_self_member_ref(lval): |
| self.fail("Final can be only applied to a name or an attribute on self", s) |
| s.is_final_def = False |
| return |
| else: |
| assert self.function_stack |
| if self.function_stack[-1].name != "__init__": |
| self.fail("Can only declare a final attribute in class body or __init__", s) |
| s.is_final_def = False |
| return |
| |
| def store_final_status(self, s: AssignmentStmt) -> None: |
| """If this is a locally valid final declaration, set the corresponding flag on `Var`.""" |
| if s.is_final_def: |
| if len(s.lvalues) == 1 and isinstance(s.lvalues[0], RefExpr): |
| node = s.lvalues[0].node |
| if isinstance(node, Var): |
| node.is_final = True |
| if s.type: |
| node.final_value = constant_fold_expr(s.rvalue, self.cur_mod_id) |
| if self.is_class_scope() and ( |
| isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs |
| ): |
| node.final_unset_in_class = True |
| else: |
| for lval in self.flatten_lvalues(s.lvalues): |
| # Special case: we are working with an `Enum`: |
| # |
| # class MyEnum(Enum): |
| # key = 'some value' |
| # |
| # Here `key` is implicitly final. In runtime, code like |
| # |
| # MyEnum.key = 'modified' |
| # |
| # will fail with `AttributeError: Cannot reassign members.` |
| # That's why we need to replicate this. |
| if ( |
| isinstance(lval, NameExpr) |
| and isinstance(self.type, TypeInfo) |
| and self.type.is_enum |
| ): |
| cur_node = self.type.names.get(lval.name, None) |
| if ( |
| cur_node |
| and isinstance(cur_node.node, Var) |
| and not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs) |
| ): |
| # Double underscored members are writable on an `Enum`. |
| # (Except read-only `__members__` but that is handled in type checker) |
| cur_node.node.is_final = s.is_final_def = not is_dunder(cur_node.node.name) |
| |
| # Special case: deferred initialization of a final attribute in __init__. |
| # In this case we just pretend this is a valid final definition to suppress |
| # errors about assigning to final attribute. |
| if isinstance(lval, MemberExpr) and self.is_self_member_ref(lval): |
| assert self.type, "Self member outside a class" |
| cur_node = self.type.names.get(lval.name, None) |
| if cur_node and isinstance(cur_node.node, Var) and cur_node.node.is_final: |
| assert self.function_stack |
| top_function = self.function_stack[-1] |
| if ( |
| top_function.name == "__init__" |
| and cur_node.node.final_unset_in_class |
| and not cur_node.node.final_set_in_init |
| and not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs) |
| ): |
| cur_node.node.final_set_in_init = True |
| s.is_final_def = True |
| |
| def flatten_lvalues(self, lvalues: list[Expression]) -> list[Expression]: |
| res: list[Expression] = [] |
| for lv in lvalues: |
| if isinstance(lv, (TupleExpr, ListExpr)): |
| res.extend(self.flatten_lvalues(lv.items)) |
| else: |
| res.append(lv) |
| return res |
| |
| def process_type_annotation(self, s: AssignmentStmt) -> None: |
| """Analyze type annotation or infer simple literal type.""" |
| if s.type: |
| lvalue = s.lvalues[-1] |
| allow_tuple_literal = isinstance(lvalue, TupleExpr) |
| analyzed = self.anal_type(s.type, allow_tuple_literal=allow_tuple_literal) |
| # Don't store not ready types (including placeholders). |
| if analyzed is None or has_placeholder(analyzed): |
| self.defer(s) |
| return |
| s.type = analyzed |
| if ( |
| self.type |
| and self.type.is_protocol |
| and isinstance(lvalue, NameExpr) |
| and isinstance(s.rvalue, TempNode) |
| and s.rvalue.no_rhs |
| ): |
| if isinstance(lvalue.node, Var): |
| lvalue.node.is_abstract_var = True |
| else: |
| if ( |
| self.type |
| and self.type.is_protocol |
| and self.is_annotated_protocol_member(s) |
| and not self.is_func_scope() |
| ): |
| self.fail("All protocol members must have explicitly declared types", s) |
| # Set the type if the rvalue is a simple literal (even if the above error occurred). |
| if len(s.lvalues) == 1 and isinstance(s.lvalues[0], RefExpr): |
| ref_expr = s.lvalues[0] |
| safe_literal_inference = True |
| if self.type and isinstance(ref_expr, NameExpr) and len(self.type.mro) > 1: |
| # Check if there is a definition in supertype. If yes, we can't safely |
| # decide here what to infer: int or Literal[42]. |
| safe_literal_inference = self.type.mro[1].get(ref_expr.name) is None |
| if safe_literal_inference and ref_expr.is_inferred_def: |
| s.type = self.analyze_simple_literal_type(s.rvalue, s.is_final_def) |
| if s.type: |
| # Store type into nodes. |
| for lvalue in s.lvalues: |
| self.store_declared_types(lvalue, s.type) |
| |
| def is_annotated_protocol_member(self, s: AssignmentStmt) -> bool: |
| """Check whether a protocol member is annotated. |
| |
| There are some exceptions that can be left unannotated, like ``__slots__``.""" |
| return any( |
| (isinstance(lv, NameExpr) and lv.name != "__slots__" and lv.is_inferred_def) |
| for lv in s.lvalues |
| ) |
| |
| def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Type | None: |
| """Return builtins.int if rvalue is an int literal, etc. |
| |
| If this is a 'Final' context, we return "Literal[...]" instead. |
| """ |
| if self.function_stack: |
| # Skip inside a function; this is to avoid confusing |
| # the code that handles dead code due to isinstance() |
| # inside type variables with value restrictions (like |
| # AnyStr). |
| return None |
| |
| value = constant_fold_expr(rvalue, self.cur_mod_id) |
| if value is None or isinstance(value, complex): |
| return None |
| |
| if isinstance(value, bool): |
| type_name = "builtins.bool" |
| elif isinstance(value, int): |
| type_name = "builtins.int" |
| elif isinstance(value, str): |
| type_name = "builtins.str" |
| elif isinstance(value, float): |
| type_name = "builtins.float" |
| |
| typ = self.named_type_or_none(type_name) |
| if typ and is_final: |
| return typ.copy_modified(last_known_value=LiteralType(value=value, fallback=typ)) |
| return typ |
| |
| def analyze_alias( |
| self, name: str, rvalue: Expression, allow_placeholder: bool = False |
| ) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str]]: |
| """Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable). |
| |
| If yes, return the corresponding type, a list of |
| qualified type variable names for generic aliases, a set of names the alias depends on, |
| and a list of type variables if the alias is generic. |
| A schematic example for the dependencies: |
| A = int |
| B = str |
| analyze_alias(Dict[A, B])[2] == {'__main__.A', '__main__.B'} |
| """ |
| dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic()) |
| global_scope = not self.type and not self.function_stack |
| try: |
| typ = expr_to_unanalyzed_type(rvalue, self.options, self.is_stub_file) |
| except TypeTranslationError: |
| self.fail( |
| "Invalid type alias: expression is not a valid type", rvalue, code=codes.VALID_TYPE |
| ) |
| return None, [], set(), [] |
| |
| found_type_vars = typ.accept(TypeVarLikeQuery(self, self.tvar_scope)) |
| tvar_defs: list[TypeVarLikeType] = [] |
| namespace = self.qualified_name(name) |
| with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): |
| for name, tvar_expr in found_type_vars: |
| tvar_def = self.tvar_scope.bind_new(name, tvar_expr) |
| tvar_defs.append(tvar_def) |
| |
| analyzed, depends_on = analyze_type_alias( |
| typ, |
| self, |
| self.tvar_scope, |
| self.plugin, |
| self.options, |
| self.is_typeshed_stub_file, |
| allow_placeholder=allow_placeholder, |
| in_dynamic_func=dynamic, |
| global_scope=global_scope, |
| allowed_alias_tvars=tvar_defs, |
| ) |
| |
| # There can be only one variadic variable at most, the error is reported elsewhere. |
| new_tvar_defs = [] |
| variadic = False |
| for td in tvar_defs: |
| if isinstance(td, TypeVarTupleType): |
| if variadic: |
| continue |
| variadic = True |
| new_tvar_defs.append(td) |
| |
| qualified_tvars = [node.fullname for _name, node in found_type_vars] |
| return analyzed, new_tvar_defs, depends_on, qualified_tvars |
| |
| def is_pep_613(self, s: AssignmentStmt) -> bool: |
| if s.unanalyzed_type is not None and isinstance(s.unanalyzed_type, UnboundType): |
| lookup = self.lookup_qualified(s.unanalyzed_type.name, s, suppress_errors=True) |
| if lookup and lookup.fullname in TYPE_ALIAS_NAMES: |
| return True |
| return False |
| |
| def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: |
| """Check if assignment creates a type alias and set it up as needed. |
| |
| Return True if it is a type alias (even if the target is not ready), |
| or False otherwise. |
| |
| Note: the resulting types for subscripted (including generic) aliases |
| are also stored in rvalue.analyzed. |
| """ |
| if s.invalid_recursive_alias: |
| return True |
| lvalue = s.lvalues[0] |
| if len(s.lvalues) > 1 or not isinstance(lvalue, NameExpr): |
| # First rule: Only simple assignments like Alias = ... create aliases. |
| return False |
| |
| pep_613 = self.is_pep_613(s) |
| if not pep_613 and s.unanalyzed_type is not None: |
| # Second rule: Explicit type (cls: Type[A] = A) always creates variable, not alias. |
| # unless using PEP 613 `cls: TypeAlias = A` |
| return False |
| |
| if isinstance(s.rvalue, CallExpr) and s.rvalue.analyzed: |
| return False |
| |
| existing = self.current_symbol_table().get(lvalue.name) |
| # Third rule: type aliases can't be re-defined. For example: |
| # A: Type[float] = int |
| # A = float # OK, but this doesn't define an alias |
| # B = int |
| # B = float # Error! |
| # Don't create an alias in these cases: |
| if existing and ( |
| isinstance(existing.node, Var) # existing variable |
| or (isinstance(existing.node, TypeAlias) and not s.is_alias_def) # existing alias |
| or (isinstance(existing.node, PlaceholderNode) and existing.node.node.line < s.line) |
| ): # previous incomplete definition |
| # TODO: find a more robust way to track the order of definitions. |
| # Note: if is_alias_def=True, this is just a node from previous iteration. |
| if isinstance(existing.node, TypeAlias) and not s.is_alias_def: |
| self.fail( |
| 'Cannot assign multiple types to name "{}"' |
| ' without an explicit "Type[...]" annotation'.format(lvalue.name), |
| lvalue, |
| ) |
| return False |
| |
| non_global_scope = self.type or self.is_func_scope() |
| if not pep_613 and isinstance(s.rvalue, RefExpr) and non_global_scope: |
| # Fourth rule (special case): Non-subscripted right hand side creates a variable |
| # at class and function scopes. For example: |
| # |
| # class Model: |
| # ... |
| # class C: |
| # model = Model # this is automatically a variable with type 'Type[Model]' |
| # |
| # without this rule, this typical use case will require a lot of explicit |
| # annotations (see the second rule). |
| return False |
| rvalue = s.rvalue |
| if not pep_613 and not self.can_be_type_alias(rvalue): |
| return False |
| |
| if existing and not isinstance(existing.node, (PlaceholderNode, TypeAlias)): |
| # Cannot redefine existing node as type alias. |
| return False |
| |
| res: Type | None = None |
| if self.is_none_alias(rvalue): |
| res = NoneType() |
| alias_tvars: list[TypeVarLikeType] = [] |
| depends_on: set[str] = set() |
| qualified_tvars: list[str] = [] |
| else: |
| tag = self.track_incomplete_refs() |
| res, alias_tvars, depends_on, qualified_tvars = self.analyze_alias( |
| lvalue.name, rvalue, allow_placeholder=True |
| ) |
| if not res: |
| return False |
| if not self.options.disable_recursive_aliases and not self.is_func_scope(): |
| # Only marking incomplete for top-level placeholders makes recursive aliases like |
| # `A = Sequence[str | A]` valid here, similar to how we treat base classes in class |
| # definitions, allowing `class str(Sequence[str]): ...` |
| incomplete_target = isinstance(res, ProperType) and isinstance( |
| res, PlaceholderType |
| ) |
| else: |
| incomplete_target = has_placeholder(res) |
| if self.found_incomplete_ref(tag) or incomplete_target: |
| # Since we have got here, we know this must be a type alias (incomplete refs |
| # may appear in nested positions), therefore use becomes_typeinfo=True. |
| self.mark_incomplete(lvalue.name, rvalue, becomes_typeinfo=True) |
| return True |
| self.add_type_alias_deps(depends_on) |
| # In addition to the aliases used, we add deps on unbound |
| # type variables, since they are erased from target type. |
| self.add_type_alias_deps(qualified_tvars) |
| # The above are only direct deps on other aliases. |
| # For subscripted aliases, type deps from expansion are added in deps.py |
| # (because the type is stored). |
| check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg, context=s) |
| # When this type alias gets "inlined", the Any is not explicit anymore, |
| # so we need to replace it with non-explicit Anys. |
| res = make_any_non_explicit(res) |
| # Note: with the new (lazy) type alias representation we only need to set no_args to True |
| # if the expected number of arguments is non-zero, so that aliases like A = List work. |
| # However, eagerly expanding aliases like Text = str is a nice performance optimization. |
| no_args = isinstance(res, Instance) and not res.args # type: ignore[misc] |
| fix_instance_types(res, self.fail, self.note, self.options) |
| # Aliases defined within functions can't be accessed outside |
| # the function, since the symbol table will no longer |
| # exist. Work around by expanding them eagerly when used. |
| eager = self.is_func_scope() |
| alias_node = TypeAlias( |
| res, |
| self.qualified_name(lvalue.name), |
| s.line, |
| s.column, |
| alias_tvars=alias_tvars, |
| no_args=no_args, |
| eager=eager, |
| ) |
| if isinstance(s.rvalue, (IndexExpr, CallExpr, OpExpr)) and ( |
| not isinstance(rvalue, OpExpr) |
| or (self.options.python_version >= (3, 10) or self.is_stub_file) |
| ): |
| # Note: CallExpr is for "void = type(None)" and OpExpr is for "X | Y" union syntax. |
| s.rvalue.analyzed = TypeAliasExpr(alias_node) |
| s.rvalue.analyzed.line = s.line |
| # we use the column from resulting target, to get better location for errors |
| s.rvalue.analyzed.column = res.column |
| elif isinstance(s.rvalue, RefExpr): |
| s.rvalue.is_alias_rvalue = True |
| |
| if existing: |
| # An alias gets updated. |
| updated = False |
| if isinstance(existing.node, TypeAlias): |
| if existing.node.target != res: |
| # Copy expansion to the existing alias, this matches how we update base classes |
| # for a TypeInfo _in place_ if there are nested placeholders. |
| existing.node.target = res |
| existing.node.alias_tvars = alias_tvars |
| existing.node.no_args = no_args |
| updated = True |
| else: |
| # Otherwise just replace existing placeholder with type alias. |
| existing.node = alias_node |
| updated = True |
| if updated: |
| if self.final_iteration: |
| self.cannot_resolve_name(lvalue.name, "name", s) |
| return True |
| else: |
| # We need to defer so that this change can get propagated to base classes. |
| self.defer(s, force_progress=True) |
| else: |
| self.add_symbol(lvalue.name, alias_node, s) |
| if isinstance(rvalue, RefExpr) and isinstance(rvalue.node, TypeAlias): |
| alias_node.normalized = rvalue.node.normalized |
| current_node = existing.node if existing else alias_node |
| assert isinstance(current_node, TypeAlias) |
| self.disable_invalid_recursive_aliases(s, current_node) |
| if self.is_class_scope(): |
| assert self.type is not None |
| if self.type.is_protocol: |
| self.fail("Type aliases are prohibited in protocol bodies", s) |
| if not lvalue.name[0].isupper(): |
| self.note("Use variable annotation syntax to define protocol members", s) |
| return True |
| |
| def disable_invalid_recursive_aliases( |
| self, s: AssignmentStmt, current_node: TypeAlias |
| ) -> None: |
| """Prohibit and fix recursive type aliases that are invalid/unsupported.""" |
| messages = [] |
| if is_invalid_recursive_alias({current_node}, current_node.target): |
| messages.append("Invalid recursive alias: a union item of itself") |
| if detect_diverging_alias( |
| current_node, current_node.target, self.lookup_qualified, self.tvar_scope |
| ): |
| messages.append("Invalid recursive alias: type variable nesting on right hand side") |
| if messages: |
| current_node.target = AnyType(TypeOfAny.from_error) |
| s.invalid_recursive_alias = True |
| for msg in messages: |
| self.fail(msg, s.rvalue) |
| |
| def analyze_lvalue( |
| self, |
| lval: Lvalue, |
| nested: bool = False, |
| explicit_type: bool = False, |
| is_final: bool = False, |
| escape_comprehensions: bool = False, |
| has_explicit_value: bool = False, |
| ) -> None: |
| """Analyze an lvalue or assignment target. |
| |
| Args: |
| lval: The target lvalue |
| nested: If true, the lvalue is within a tuple or list lvalue expression |
| explicit_type: Assignment has type annotation |
| escape_comprehensions: If we are inside a comprehension, set the variable |
| in the enclosing scope instead. This implements |
| https://www.python.org/dev/peps/pep-0572/#scope-of-the-target |
| """ |
| if escape_comprehensions: |
| assert isinstance(lval, NameExpr), "assignment expression target must be NameExpr" |
| if isinstance(lval, NameExpr): |
| self.analyze_name_lvalue( |
| lval, |
| explicit_type, |
| is_final, |
| escape_comprehensions, |
| has_explicit_value=has_explicit_value, |
| ) |
| elif isinstance(lval, MemberExpr): |
| self.analyze_member_lvalue(lval, explicit_type, is_final, has_explicit_value) |
| if explicit_type and not self.is_self_member_ref(lval): |
| self.fail("Type cannot be declared in assignment to non-self attribute", lval) |
| elif isinstance(lval, IndexExpr): |
| if explicit_type: |
| self.fail("Unexpected type declaration", lval) |
| lval.accept(self) |
| elif isinstance(lval, TupleExpr): |
| self.analyze_tuple_or_list_lvalue(lval, explicit_type) |
| elif isinstance(lval, StarExpr): |
| if nested: |
| self.analyze_lvalue(lval.expr, nested, explicit_type) |
| else: |
| self.fail("Starred assignment target must be in a list or tuple", lval) |
| else: |
| self.fail("Invalid assignment target", lval) |
| |
| def analyze_name_lvalue( |
| self, |
| lvalue: NameExpr, |
| explicit_type: bool, |
| is_final: bool, |
| escape_comprehensions: bool, |
| has_explicit_value: bool, |
| ) -> None: |
| """Analyze an lvalue that targets a name expression. |
| |
| Arguments are similar to "analyze_lvalue". |
| """ |
| if lvalue.node: |
| # This has been bound already in a previous iteration. |
| return |
| |
| name = lvalue.name |
| if self.is_alias_for_final_name(name): |
| if is_final: |
| self.fail("Cannot redefine an existing name as final", lvalue) |
| else: |
| self.msg.cant_assign_to_final(name, self.type is not None, lvalue) |
| |
| kind = self.current_symbol_kind() |
| names = self.current_symbol_table(escape_comprehensions=escape_comprehensions) |
| existing = names.get(name) |
| |
| outer = self.is_global_or_nonlocal(name) |
| if kind == MDEF and isinstance(self.type, TypeInfo) and self.type.is_enum: |
| # Special case: we need to be sure that `Enum` keys are unique. |
| if existing is not None and not isinstance(existing.node, PlaceholderNode): |
| self.fail( |
| 'Attempted to reuse member name "{}" in Enum definition "{}"'.format( |
| name, self.type.name |
| ), |
| lvalue, |
| ) |
| |
| if (not existing or isinstance(existing.node, PlaceholderNode)) and not outer: |
| # Define new variable. |
| var = self.make_name_lvalue_var(lvalue, kind, not explicit_type, has_explicit_value) |
| added = self.add_symbol(name, var, lvalue, escape_comprehensions=escape_comprehensions) |
| # Only bind expression if we successfully added name to symbol table. |
| if added: |
| lvalue.is_new_def = True |
| lvalue.is_inferred_def = True |
| lvalue.kind = kind |
| lvalue.node = var |
| if kind == GDEF: |
| lvalue.fullname = var._fullname |
| else: |
| lvalue.fullname = lvalue.name |
| if self.is_func_scope(): |
| if unmangle(name) == "_": |
| # Special case for assignment to local named '_': always infer 'Any'. |
| typ = AnyType(TypeOfAny.special_form) |
| self.store_declared_types(lvalue, typ) |
| if is_final and self.is_final_redefinition(kind, name): |
| self.fail("Cannot redefine an existing name as final", lvalue) |
| else: |
| self.make_name_lvalue_point_to_existing_def(lvalue, explicit_type, is_final) |
| |
| def is_final_redefinition(self, kind: int, name: str) -> bool: |
| if kind == GDEF: |
| return self.is_mangled_global(name) and not self.is_initial_mangled_global(name) |
| elif kind == MDEF and self.type: |
| return unmangle(name) + "'" in self.type.names |
| return False |
| |
| def is_alias_for_final_name(self, name: str) -> bool: |
| if self.is_func_scope(): |
| if not name.endswith("'"): |
| # Not a mangled name -- can't be an alias |
| return False |
| name = unmangle(name) |
| assert self.locals[-1] is not None, "No locals at function scope" |
| existing = self.locals[-1].get(name) |
| return existing is not None and is_final_node(existing.node) |
| elif self.type is not None: |
| orig_name = unmangle(name) + "'" |
| if name == orig_name: |
| return False |
| existing = self.type.names.get(orig_name) |
| return existing is not None and is_final_node(existing.node) |
| else: |
| orig_name = unmangle(name) + "'" |
| if name == orig_name: |
| return False |
| existing = self.globals.get(orig_name) |
| return existing is not None and is_final_node(existing.node) |
| |
| def make_name_lvalue_var( |
| self, lvalue: NameExpr, kind: int, inferred: bool, has_explicit_value: bool |
| ) -> Var: |
| """Return a Var node for an lvalue that is a name expression.""" |
| name = lvalue.name |
| v = Var(name) |
| v.set_line(lvalue) |
| v.is_inferred = inferred |
| if kind == MDEF: |
| assert self.type is not None |
| v.info = self.type |
| v.is_initialized_in_class = True |
| v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_OVERRIDE |
| if kind != LDEF: |
| v._fullname = self.qualified_name(name) |
| else: |
| # fullanme should never stay None |
| v._fullname = name |
| v.is_ready = False # Type not inferred yet |
| v.has_explicit_value = has_explicit_value |
| return v |
| |
| def make_name_lvalue_point_to_existing_def( |
| self, lval: NameExpr, explicit_type: bool, is_final: bool |
| ) -> None: |
| """Update an lvalue to point to existing definition in the same scope. |
| |
| Arguments are similar to "analyze_lvalue". |
| |
| Assume that an existing name exists. |
| """ |
| if is_final: |
| # Redefining an existing name with final is always an error. |
| self.fail("Cannot redefine an existing name as final", lval) |
| original_def = self.lookup(lval.name, lval, suppress_errors=True) |
| if original_def is None and self.type and not self.is_func_scope(): |
| # Workaround to allow "x, x = ..." in class body. |
| original_def = self.type.get(lval.name) |
| if explicit_type: |
| # Don't re-bind if there is a type annotation. |
| self.name_already_defined(lval.name, lval, original_def) |
| else: |
| # Bind to an existing name. |
| if original_def: |
| self.bind_name_expr(lval, original_def) |
| else: |
| self.name_not_defined(lval.name, lval) |
| self.check_lvalue_validity(lval.node, lval) |
| |
| def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, explicit_type: bool = False) -> None: |
| """Analyze an lvalue or assignment target that is a list or tuple.""" |
| items = lval.items |
| star_exprs = [item for item in items if isinstance(item, StarExpr)] |
| |
| if len(star_exprs) > 1: |
| self.fail("Two starred expressions in assignment", lval) |
| else: |
| if len(star_exprs) == 1: |
| star_exprs[0].valid = True |
| for i in items: |
| self.analyze_lvalue( |
| lval=i, |
| nested=True, |
| explicit_type=explicit_type, |
| # Lists and tuples always have explicit values defined: |
| # `a, b, c = value` |
| has_explicit_value=True, |
| ) |
| |
| def analyze_member_lvalue( |
| self, lval: MemberExpr, explicit_type: bool, is_final: bool, has_explicit_value: bool |
| ) -> None: |
| """Analyze lvalue that is a member expression. |
| |
| Arguments: |
| lval: The target lvalue |
| explicit_type: Assignment has type annotation |
| is_final: Is the target final |
| """ |
| if lval.node: |
| # This has been bound already in a previous iteration. |
| return |
| lval.accept(self) |
| if self.is_self_member_ref(lval): |
| assert self.type, "Self member outside a class" |
| cur_node = self.type.names.get(lval.name) |
| node = self.type.get(lval.name) |
| if cur_node and is_final: |
| # Overrides will be checked in type checker. |
| self.fail("Cannot redefine an existing name as final", lval) |
| # On first encounter with this definition, if this attribute was defined before |
| # with an inferred type and it's marked with an explicit type now, give an error. |
| if ( |
| not lval.node |
| and cur_node |
| and isinstance(cur_node.node, Var) |
| and cur_node.node.is_inferred |
| and explicit_type |
| ): |
| self.attribute_already_defined(lval.name, lval, cur_node) |
| if self.type.is_protocol and has_explicit_value and cur_node is not None: |
| # Make this variable non-abstract, it would be safer to do this only if we |
| # are inside __init__, but we do this always to preserve historical behaviour. |
| if isinstance(cur_node.node, Var): |
| cur_node.node.is_abstract_var = False |
| if ( |
| # If the attribute of self is not defined, create a new Var, ... |
| node is None |
| # ... or if it is defined as abstract in a *superclass*. |
| or (cur_node is None and isinstance(node.node, Var) and node.node.is_abstract_var) |
| # ... also an explicit declaration on self also creates a new Var. |
| # Note that `explicit_type` might have been erased for bare `Final`, |
| # so we also check if `is_final` is passed. |
| or (cur_node is None and (explicit_type or is_final)) |
| ): |
| if self.type.is_protocol and node is None: |
| self.fail("Protocol members cannot be defined via assignment to self", lval) |
| else: |
| # Implicit attribute definition in __init__. |
| lval.is_new_def = True |
| lval.is_inferred_def = True |
| v = Var(lval.name) |
| v.set_line(lval) |
| v._fullname = self.qualified_name(lval.name) |
| v.info = self.type |
| v.is_ready = False |
| v.explicit_self_type = explicit_type or is_final |
| lval.def_var = v |
| lval.node = v |
| # TODO: should we also set lval.kind = MDEF? |
| self.type.names[lval.name] = SymbolTableNode(MDEF, v, implicit=True) |
| self.check_lvalue_validity(lval.node, lval) |
| |
| def is_self_member_ref(self, memberexpr: MemberExpr) -> bool: |
| """Does memberexpr to refer to an attribute of self?""" |
| if not isinstance(memberexpr.expr, NameExpr): |
| return False |
| node = memberexpr.expr.node |
| return isinstance(node, Var) and node.is_self |
| |
| def check_lvalue_validity(self, node: Expression | SymbolNode | None, ctx: Context) -> None: |
| if isinstance(node, TypeVarExpr): |
| self.fail("Invalid assignment target", ctx) |
| elif isinstance(node, TypeInfo): |
| self.fail(message_registry.CANNOT_ASSIGN_TO_TYPE, ctx) |
| |
| def store_declared_types(self, lvalue: Lvalue, typ: Type) -> None: |
| if isinstance(lvalue, RefExpr): |
| lvalue.is_inferred_def = False |
| if isinstance(lvalue.node, Var): |
| var = lvalue.node |
| var.type = typ |
| var.is_ready = True |
| typ = get_proper_type(typ) |
| if ( |
| var.is_final |
| and isinstance(typ, Instance) |
| and typ.last_known_value |
| and (not self.type or not self.type.is_enum) |
| ): |
| var.final_value = typ.last_known_value.value |
| # If node is not a variable, we'll catch it elsewhere. |
| elif isinstance(lvalue, TupleExpr): |
| typ = get_proper_type(typ) |
| if isinstance(typ, TupleType): |
| if len(lvalue.items) != len(typ.items): |
| self.fail("Incompatible number of tuple items", lvalue) |
| return |
| for item, itemtype in zip(lvalue.items, typ.items): |
| self.store_declared_types(item, itemtype) |
| else: |
| self.fail("Tuple type expected for multiple variables", lvalue) |
| elif isinstance(lvalue, StarExpr): |
| # Historical behavior for the old parser |
| self.store_declared_types(lvalue.expr, typ) |
| else: |
| # This has been flagged elsewhere as an error, so just ignore here. |
| pass |
| |
| def process_typevar_declaration(self, s: AssignmentStmt) -> bool: |
| """Check if s declares a TypeVar; it yes, store it in symbol table. |
| |
| Return True if this looks like a type variable declaration (but maybe |
| with errors), otherwise return False. |
| """ |
| call = self.get_typevarlike_declaration(s, ("typing.TypeVar", "typing_extensions.TypeVar")) |
| if not call: |
| return False |
| |
| name = self.extract_typevarlike_name(s, call) |
| if name is None: |
| return False |
| |
| # Constraining types |
| n_values = call.arg_kinds[1:].count(ARG_POS) |
| values = self.analyze_value_types(call.args[1 : 1 + n_values]) |
| |
| res = self.process_typevar_parameters( |
| call.args[1 + n_values :], |
| call.arg_names[1 + n_values :], |
| call.arg_kinds[1 + n_values :], |
| n_values, |
| s, |
| ) |
| if res is None: |
| return False |
| variance, upper_bound, default = res |
| |
| existing = self.current_symbol_table().get(name) |
| if existing and not ( |
| isinstance(existing.node, PlaceholderNode) |
| or |
| # Also give error for another type variable with the same name. |
| (isinstance(existing.node, TypeVarExpr) and existing.node is call.analyzed) |
| ): |
| self.fail(f'Cannot redefine "{name}" as a type variable', s) |
| return False |
| |
| if self.options.disallow_any_unimported: |
| for idx, constraint in enumerate(values, start=1): |
| if has_any_from_unimported_type(constraint): |
| prefix = f"Constraint {idx}" |
| self.msg.unimported_type_becomes_any(prefix, constraint, s) |
| |
| if has_any_from_unimported_type(upper_bound): |
| prefix = "Upper bound of type variable" |
| self.msg.unimported_type_becomes_any(prefix, upper_bound, s) |
| |
| for t in values + [upper_bound, default]: |
| check_for_explicit_any( |
| t, self.options, self.is_typeshed_stub_file, self.msg, context=s |
| ) |
| |
| # mypyc suppresses making copies of a function to check each |
| # possible type, so set the upper bound to Any to prevent that |
| # from causing errors. |
| if values and self.options.mypyc: |
| upper_bound = AnyType(TypeOfAny.implementation_artifact) |
| |
| # Yes, it's a valid type variable definition! Add it to the symbol table. |
| if not call.analyzed: |
| type_var = TypeVarExpr( |
| name, self.qualified_name(name), values, upper_bound, default, variance |
| ) |
| type_var.line = call.line |
| call.analyzed = type_var |
| updated = True |
| else: |
| assert isinstance(call.analyzed, TypeVarExpr) |
| updated = ( |
| values != call.analyzed.values |
| or upper_bound != call.analyzed.upper_bound |
| or default != call.analyzed.default |
| ) |
| call.analyzed.upper_bound = upper_bound |
| call.analyzed.values = values |
| call.analyzed.default = default |
| if any(has_placeholder(v) for v in values): |
| self.process_placeholder(None, "TypeVar values", s, force_progress=updated) |
| elif has_placeholder(upper_bound): |
| self.process_placeholder(None, "TypeVar upper bound", s, force_progress=updated) |
| elif has_placeholder(default): |
| self.process_placeholder(None, "TypeVar default", s, force_progress=updated) |
| |
| self.add_symbol(name, call.analyzed, s) |
| return True |
| |
| def check_typevarlike_name(self, call: CallExpr, name: str, context: Context) -> bool: |
| """Checks that the name of a TypeVar or ParamSpec matches its variable.""" |
| name = unmangle(name) |
| assert isinstance(call.callee, RefExpr) |
| typevarlike_type = ( |
| call.callee.name if isinstance(call.callee, NameExpr) else call.callee.fullname |
| ) |
| if len(call.args) < 1: |
| self.fail(f"Too few arguments for {typevarlike_type}()", context) |
| return False |
| if not isinstance(call.args[0], StrExpr) or not call.arg_kinds[0] == ARG_POS: |
| self.fail(f"{typevarlike_type}() expects a string literal as first argument", context) |
| return False |
| elif call.args[0].value != name: |
| msg = 'String argument 1 "{}" to {}(...) does not match variable name "{}"' |
| self.fail(msg.format(call.args[0].value, typevarlike_type, name), context) |
| return False |
| return True |
| |
| def get_typevarlike_declaration( |
| self, s: AssignmentStmt, typevarlike_types: tuple[str, ...] |
| ) -> CallExpr | None: |
| """Returns the call expression if `s` is a declaration of `typevarlike_type` |
| (TypeVar or ParamSpec), or None otherwise. |
| """ |
| if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr): |
| return None |
| if not isinstance(s.rvalue, CallExpr): |
| return None |
| call = s.rvalue |
| callee = call.callee |
| if not isinstance(callee, RefExpr): |
| return None |
| if callee.fullname not in typevarlike_types: |
| return None |
| return call |
| |
| def process_typevar_parameters( |
| self, |
| args: list[Expression], |
| names: list[str | None], |
| kinds: list[ArgKind], |
| num_values: int, |
| context: Context, |
| ) -> tuple[int, Type, Type] | None: |
| has_values = num_values > 0 |
| covariant = False |
| contravariant = False |
| upper_bound: Type = self.object_type() |
| default: Type = AnyType(TypeOfAny.from_omitted_generics) |
| for param_value, param_name, param_kind in zip(args, names, kinds): |
| if not param_kind.is_named(): |
| self.fail(message_registry.TYPEVAR_UNEXPECTED_ARGUMENT, context) |
| return None |
| if param_name == "covariant": |
| if isinstance(param_value, NameExpr) and param_value.name in ("True", "False"): |
| covariant = param_value.name == "True" |
| else: |
| self.fail(message_registry.TYPEVAR_VARIANCE_DEF.format("covariant"), context) |
| return None |
| elif param_name == "contravariant": |
| if isinstance(param_value, NameExpr) and param_value.name in ("True", "False"): |
| contravariant = param_value.name == "True" |
| else: |
| self.fail( |
| message_registry.TYPEVAR_VARIANCE_DEF.format("contravariant"), context |
| ) |
| return None |
| elif param_name == "bound": |
| if has_values: |
| self.fail("TypeVar cannot have both values and an upper bound", context) |
| return None |
| tv_arg = self.get_typevarlike_argument("TypeVar", param_name, param_value, context) |
| if tv_arg is None: |
| return None |
| upper_bound = tv_arg |
| elif param_name == "default": |
| tv_arg = self.get_typevarlike_argument( |
| "TypeVar", param_name, param_value, context, allow_unbound_tvars=True |
| ) |
| default = tv_arg or AnyType(TypeOfAny.from_error) |
| elif param_name == "values": |
| # Probably using obsolete syntax with values=(...). Explain the current syntax. |
| self.fail('TypeVar "values" argument not supported', context) |
| self.fail( |
| "Use TypeVar('T', t, ...) instead of TypeVar('T', values=(t, ...))", context |
| ) |
| return None |
| else: |
| self.fail( |
| f'{message_registry.TYPEVAR_UNEXPECTED_ARGUMENT}: "{param_name}"', context |
| ) |
| return None |
| |
| if covariant and contravariant: |
| self.fail("TypeVar cannot be both covariant and contravariant", context) |
| return None |
| elif num_values == 1: |
| self.fail("TypeVar cannot have only a single constraint", context) |
| return None |
| elif covariant: |
| variance = COVARIANT |
| elif contravariant: |
| variance = CONTRAVARIANT |
| else: |
| variance = INVARIANT |
| return variance, upper_bound, default |
| |
| def get_typevarlike_argument( |
| self, |
| typevarlike_name: str, |
| param_name: str, |
| param_value: Expression, |
| context: Context, |
| *, |
| allow_unbound_tvars: bool = False, |
| allow_param_spec_literals: bool = False, |
| report_invalid_typevar_arg: bool = True, |
| ) -> ProperType | None: |
| try: |
| # We want to use our custom error message below, so we suppress |
| # the default error message for invalid types here. |
| analyzed = self.expr_to_analyzed_type( |
| param_value, |
| allow_placeholder=True, |
| report_invalid_types=False, |
| allow_unbound_tvars=allow_unbound_tvars, |
| allow_param_spec_literals=allow_param_spec_literals, |
| ) |
| if analyzed is None: |
| # Type variables are special: we need to place them in the symbol table |
| # soon, even if upper bound is not ready yet. Otherwise avoiding |
| # a "deadlock" in this common pattern would be tricky: |
| # T = TypeVar('T', bound=Custom[Any]) |
| # class Custom(Generic[T]): |
| # ... |
| analyzed = PlaceholderType(None, [], context.line) |
| typ = get_proper_type(analyzed) |
| if report_invalid_typevar_arg and isinstance(typ, AnyType) and typ.is_from_error: |
| self.fail( |
| message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name), |
| param_value, |
| ) |
| # Note: we do not return 'None' here -- we want to continue |
| # using the AnyType. |
| return typ |
| except TypeTranslationError: |
| if report_invalid_typevar_arg: |
| self.fail( |
| message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name), |
| param_value, |
| ) |
| return None |
| |
| def extract_typevarlike_name(self, s: AssignmentStmt, call: CallExpr) -> str | None: |
| if not call: |
| return None |
| |
| lvalue = s.lvalues[0] |
| assert isinstance(lvalue, NameExpr) |
| if s.type: |
| self.fail("Cannot declare the type of a TypeVar or similar construct", s) |
| return None |
| |
| if not self.check_typevarlike_name(call, lvalue.name, s): |
| return None |
| return lvalue.name |
| |
| def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: |
| """Checks if s declares a ParamSpec; if yes, store it in symbol table. |
| |
| Return True if this looks like a ParamSpec (maybe with errors), otherwise return False. |
| |
| In the future, ParamSpec may accept bounds and variance arguments, in which |
| case more aggressive sharing of code with process_typevar_declaration should be pursued. |
| """ |
| call = self.get_typevarlike_declaration( |
| s, ("typing_extensions.ParamSpec", "typing.ParamSpec") |
| ) |
| if not call: |
| return False |
| |
| name = self.extract_typevarlike_name(s, call) |
| if name is None: |
| return False |
| |
| n_values = call.arg_kinds[1:].count(ARG_POS) |
| if n_values != 0: |
| self.fail('Too many positional arguments for "ParamSpec"', s) |
| |
| default: Type = AnyType(TypeOfAny.from_omitted_generics) |
| for param_value, param_name in zip( |
| call.args[1 + n_values :], call.arg_names[1 + n_values :] |
| ): |
| if param_name == "default": |
| tv_arg = self.get_typevarlike_argument( |
| "ParamSpec", |
| param_name, |
| param_value, |
| s, |
| allow_unbound_tvars=True, |
| allow_param_spec_literals=True, |
| report_invalid_typevar_arg=False, |
| ) |
| default = tv_arg or AnyType(TypeOfAny.from_error) |
| if isinstance(tv_arg, Parameters): |
| for i, arg_type in enumerate(tv_arg.arg_types): |
| typ = get_proper_type(arg_type) |
| if isinstance(typ, AnyType) and typ.is_from_error: |
| self.fail( |
| f"Argument {i} of ParamSpec default must be a type", param_value |
| ) |
| elif ( |
| isinstance(default, AnyType) |
| and default.is_from_error |
| or not isinstance(default, (AnyType, UnboundType)) |
| ): |
| self.fail( |
| "The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec", |
| param_value, |
| ) |
| default = AnyType(TypeOfAny.from_error) |
| else: |
| # ParamSpec is different from a regular TypeVar: |
| # arguments are not semantically valid. But, allowed in runtime. |
| # So, we need to warn users about possible invalid usage. |
| self.fail( |
| "The variance and bound arguments to ParamSpec do not have defined semantics yet", |
| s, |
| ) |
| |
| # PEP 612 reserves the right to define bound, covariant and contravariant arguments to |
| # ParamSpec in a later PEP. If and when that happens, we should do something |
| # on the lines of process_typevar_parameters |
| |
| if not call.analyzed: |
| paramspec_var = ParamSpecExpr( |
| name, self.qualified_name(name), self.object_type(), default, INVARIANT |
| ) |
| paramspec_var.line = call.line |
| call.analyzed = paramspec_var |
| updated = True |
| else: |
| assert isinstance(call.analyzed, ParamSpecExpr) |
| updated = default != call.analyzed.default |
| call.analyzed.default = default |
| if has_placeholder(default): |
| self.process_placeholder(None, "ParamSpec default", s, force_progress=updated) |
| |
| self.add_symbol(name, call.analyzed, s) |
| return True |
| |
| def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: |
| """Checks if s declares a TypeVarTuple; if yes, store it in symbol table. |
| |
| Return True if this looks like a TypeVarTuple (maybe with errors), otherwise return False. |
| """ |
| call = self.get_typevarlike_declaration( |
| s, ("typing_extensions.TypeVarTuple", "typing.TypeVarTuple") |
| ) |
| if not call: |
| return False |
| |
| n_values = call.arg_kinds[1:].count(ARG_POS) |
| if n_values != 0: |
| self.fail('Too many positional arguments for "TypeVarTuple"', s) |
| |
| default: Type = AnyType(TypeOfAny.from_omitted_generics) |
| for param_value, param_name in zip( |
| call.args[1 + n_values :], call.arg_names[1 + n_values :] |
| ): |
| if param_name == "default": |
| tv_arg = self.get_typevarlike_argument( |
| "TypeVarTuple", |
| param_name, |
| param_value, |
| s, |
| allow_unbound_tvars=True, |
| report_invalid_typevar_arg=False, |
| ) |
| default = tv_arg or AnyType(TypeOfAny.from_error) |
| if not isinstance(default, UnpackType): |
| self.fail( |
| "The default argument to TypeVarTuple must be an Unpacked tuple", |
| param_value, |
| ) |
| default = AnyType(TypeOfAny.from_error) |
| else: |
| self.fail(f'Unexpected keyword argument "{param_name}" for "TypeVarTuple"', s) |
| |
| if not self.incomplete_feature_enabled(TYPE_VAR_TUPLE, s): |
| return False |
| |
| name = self.extract_typevarlike_name(s, call) |
| if name is None: |
| return False |
| |
| # PEP 646 does not specify the behavior of variance, constraints, or bounds. |
| if not call.analyzed: |
| tuple_fallback = self.named_type("builtins.tuple", [self.object_type()]) |
| typevartuple_var = TypeVarTupleExpr( |
| name, |
| self.qualified_name(name), |
| self.object_type(), |
| tuple_fallback, |
| default, |
| INVARIANT, |
| ) |
| typevartuple_var.line = call.line |
| call.analyzed = typevartuple_var |
| updated = True |
| else: |
| assert isinstance(call.analyzed, TypeVarTupleExpr) |
| updated = default != call.analyzed.default |
| call.analyzed.default = default |
| if has_placeholder(default): |
| self.process_placeholder(None, "TypeVarTuple default", s, force_progress=updated) |
| |
| self.add_symbol(name, call.analyzed, s) |
| return True |
| |
| def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance, line: int) -> TypeInfo: |
| if self.is_func_scope() and not self.type and "@" not in name: |
| name += "@" + str(line) |
| class_def = ClassDef(name, Block([])) |
| if self.is_func_scope() and not self.type: |
| # Full names of generated classes should always be prefixed with the module names |
| # even if they are nested in a function, since these classes will be (de-)serialized. |
| # (Note that the caller should append @line to the name to avoid collisions.) |
| # TODO: clean this up, see #6422. |
| class_def.fullname = self.cur_mod_id + "." + self.qualified_name(name) |
| else: |
| class_def.fullname = self.qualified_name(name) |
| |
| info = TypeInfo(SymbolTable(), class_def, self.cur_mod_id) |
| class_def.info = info |
| mro = basetype_or_fallback.type.mro |
| if not mro: |
| # Probably an error, we should not crash so generate something meaningful. |
| mro = [basetype_or_fallback.type, self.object_type().type] |
| info.mro = [info] + mro |
| info.bases = [basetype_or_fallback] |
| return info |
| |
| def analyze_value_types(self, items: list[Expression]) -> list[Type]: |
| """Analyze types from values expressions in type variable definition.""" |
| result: list[Type] = [] |
| for node in items: |
| try: |
| analyzed = self.anal_type( |
| self.expr_to_unanalyzed_type(node), allow_placeholder=True |
| ) |
| if analyzed is None: |
| # Type variables are special: we need to place them in the symbol table |
| # soon, even if some value is not ready yet, see process_typevar_parameters() |
| # for an example. |
| analyzed = PlaceholderType(None, [], node.line) |
| result.append(analyzed) |
| except TypeTranslationError: |
| self.fail("Type expected", node) |
| result.append(AnyType(TypeOfAny.from_error)) |
| return result |
| |
| def check_classvar(self, s: AssignmentStmt) -> None: |
| """Check if assignment defines a class variable.""" |
| lvalue = s.lvalues[0] |
| if len(s.lvalues) != 1 or not isinstance(lvalue, RefExpr): |
| return |
| if not s.type or not self.is_classvar(s.type): |
| return |
| if self.is_class_scope() and isinstance(lvalue, NameExpr): |
| node = lvalue.node |
| if isinstance(node, Var): |
| node.is_classvar = True |
| analyzed = self.anal_type(s.type) |
| assert self.type is not None |
| if analyzed is not None and set(get_type_vars(analyzed)) & set( |
| self.type.defn.type_vars |
| ): |
| # This means that we have a type var defined inside of a ClassVar. |
| # This is not allowed by PEP526. |
| # See https://github.com/python/mypy/issues/11538 |
| |
| self.fail(message_registry.CLASS_VAR_WITH_TYPEVARS, s) |
| if ( |
| analyzed is not None |
| and self.type.self_type in get_type_vars(analyzed) |
| and self.type.defn.type_vars |
| ): |
| self.fail(message_registry.CLASS_VAR_WITH_GENERIC_SELF, s) |
| elif not isinstance(lvalue, MemberExpr) or self.is_self_member_ref(lvalue): |
| # In case of member access, report error only when assigning to self |
| # Other kinds of member assignments should be already reported |
| self.fail_invalid_classvar(lvalue) |
| |
| def is_classvar(self, typ: Type) -> bool: |
| if not isinstance(typ, UnboundType): |
| return False |
| sym = self.lookup_qualified(typ.name, typ) |
| if not sym or not sym.node: |
| return False |
| return sym.node.fullname == "typing.ClassVar" |
| |
| def is_final_type(self, typ: Type | None) -> bool: |
| if not isinstance(typ, UnboundType): |
| return False |
| sym = self.lookup_qualified(typ.name, typ) |
| if not sym or not sym.node: |
| return False |
| return sym.node.fullname in FINAL_TYPE_NAMES |
| |
| def fail_invalid_classvar(self, context: Context) -> None: |
| self.fail(message_registry.CLASS_VAR_OUTSIDE_OF_CLASS, context) |
| |
| def process_module_assignment( |
| self, lvals: list[Lvalue], rval: Expression, ctx: AssignmentStmt |
| ) -> None: |
| """Propagate module references across assignments. |
| |
| Recursively handles the simple form of iterable unpacking; doesn't |
| handle advanced unpacking with *rest, dictionary unpacking, etc. |
| |
| In an expression like x = y = z, z is the rval and lvals will be [x, |
| y]. |
| |
| """ |
| if isinstance(rval, (TupleExpr, ListExpr)) and all( |
| isinstance(v, TupleExpr) for v in lvals |
| ): |
| # rval and all lvals are either list or tuple, so we are dealing |
| # with unpacking assignment like `x, y = a, b`. Mypy didn't |
| # understand our all(isinstance(...)), so cast them as TupleExpr |
| # so mypy knows it is safe to access their .items attribute. |
| seq_lvals = cast(List[TupleExpr], lvals) |
| # given an assignment like: |
| # (x, y) = (m, n) = (a, b) |
| # we now have: |
| # seq_lvals = [(x, y), (m, n)] |
| # seq_rval = (a, b) |
| # We now zip this into: |
| # elementwise_assignments = [(a, x, m), (b, y, n)] |
| # where each elementwise assignment includes one element of rval and the |
| # corresponding element of each lval. Basically we unpack |
| # (x, y) = (m, n) = (a, b) |
| # into elementwise assignments |
| # x = m = a |
| # y = n = b |
| # and then we recursively call this method for each of those assignments. |
| # If the rval and all lvals are not all of the same length, zip will just ignore |
| # extra elements, so no error will be raised here; mypy will later complain |
| # about the length mismatch in type-checking. |
| elementwise_assignments = zip(rval.items, *[v.items for v in seq_lvals]) |
| for rv, *lvs in elementwise_assignments: |
| self.process_module_assignment(lvs, rv, ctx) |
| elif isinstance(rval, RefExpr): |
| rnode = self.lookup_type_node(rval) |
| if rnode and isinstance(rnode.node, MypyFile): |
| for lval in lvals: |
| if not isinstance(lval, RefExpr): |
| continue |
| # respect explicitly annotated type |
| if isinstance(lval.node, Var) and lval.node.type is not None: |
| continue |
| |
| # We can handle these assignments to locals and to self |
| if isinstance(lval, NameExpr): |
| lnode = self.current_symbol_table().get(lval.name) |
| elif isinstance(lval, MemberExpr) and self.is_self_member_ref(lval): |
| assert self.type is not None |
| lnode = self.type.names.get(lval.name) |
| else: |
| continue |
| |
| if lnode: |
| if isinstance(lnode.node, MypyFile) and lnode.node is not rnode.node: |
| assert isinstance(lval, (NameExpr, MemberExpr)) |
| self.fail( |
| 'Cannot assign multiple modules to name "{}" ' |
| 'without explicit "types.ModuleType" annotation'.format(lval.name), |
| ctx, |
| ) |
| # never create module alias except on initial var definition |
| elif lval.is_inferred_def: |
| assert rnode.node is not None |
| lnode.node = rnode.node |
| |
| def process__all__(self, s: AssignmentStmt) -> None: |
| """Export names if argument is a __all__ assignment.""" |
| if ( |
| len(s.lvalues) == 1 |
| and isinstance(s.lvalues[0], NameExpr) |
| and s.lvalues[0].name == "__all__" |
| and s.lvalues[0].kind == GDEF |
| and isinstance(s.rvalue, (ListExpr, TupleExpr)) |
| ): |
| self.add_exports(s.rvalue.items) |
| |
| def process__deletable__(self, s: AssignmentStmt) -> None: |
| if not self.options.mypyc: |
| return |
| if ( |
| len(s.lvalues) == 1 |
| and isinstance(s.lvalues[0], NameExpr) |
| and s.lvalues[0].name == "__deletable__" |
| and s.lvalues[0].kind == MDEF |
| ): |
| rvalue = s.rvalue |
| if not isinstance(rvalue, (ListExpr, TupleExpr)): |
| self.fail('"__deletable__" must be initialized with a list or tuple expression', s) |
| return |
| items = rvalue.items |
| attrs = [] |
| for item in items: |
| if not isinstance(item, StrExpr): |
| self.fail('Invalid "__deletable__" item; string literal expected', item) |
| else: |
| attrs.append(item.value) |
| assert self.type |
| self.type.deletable_attributes = attrs |
| |
| def process__slots__(self, s: AssignmentStmt) -> None: |
| """ |
| Processing ``__slots__`` if defined in type. |
| |
| See: https://docs.python.org/3/reference/datamodel.html#slots |
| """ |
| # Later we can support `__slots__` defined as `__slots__ = other = ('a', 'b')` |
| if ( |
| isinstance(self.type, TypeInfo) |
| and len(s.lvalues) == 1 |
| and isinstance(s.lvalues[0], NameExpr) |
| and s.lvalues[0].name == "__slots__" |
| and s.lvalues[0].kind == MDEF |
| ): |
| # We understand `__slots__` defined as string, tuple, list, set, and dict: |
| if not isinstance(s.rvalue, (StrExpr, ListExpr, TupleExpr, SetExpr, DictExpr)): |
| # For example, `__slots__` can be defined as a variable, |
| # we don't support it for now. |
| return |
| |
| if any(p.slots is None for p in self.type.mro[1:-1]): |
| # At least one type in mro (excluding `self` and `object`) |
| # does not have concrete `__slots__` defined. Ignoring. |
| return |
| |
| concrete_slots = True |
| rvalue: list[Expression] = [] |
| if isinstance(s.rvalue, StrExpr): |
| rvalue.append(s.rvalue) |
| elif isinstance(s.rvalue, (ListExpr, TupleExpr, SetExpr)): |
| rvalue.extend(s.rvalue.items) |
| else: |
| # We have a special treatment of `dict` with possible `{**kwargs}` usage. |
| # In this case we consider all `__slots__` to be non-concrete. |
| for key, _ in s.rvalue.items: |
| if concrete_slots and key is not None: |
| rvalue.append(key) |
| else: |
| concrete_slots = False |
| |
| slots = [] |
| for item in rvalue: |
| # Special case for `'__dict__'` value: |
| # when specified it will still allow any attribute assignment. |
| if isinstance(item, StrExpr) and item.value != "__dict__": |
| slots.append(item.value) |
| else: |
| concrete_slots = False |
| if not concrete_slots: |
| # Some slot items are dynamic, we don't want any false positives, |
| # so, we just pretend that this type does not have any slots at all. |
| return |
| |
| # We need to copy all slots from super types: |
| for super_type in self.type.mro[1:-1]: |
| assert super_type.slots is not None |
| slots.extend(super_type.slots) |
| self.type.slots = set(slots) |
| |
| # |
| # Misc statements |
| # |
| |
| def visit_block(self, b: Block) -> None: |
| if b.is_unreachable: |
| return |
| self.block_depth[-1] += 1 |
| for s in b.body: |
| self.accept(s) |
| self.block_depth[-1] -= 1 |
| |
| def visit_block_maybe(self, b: Block | None) -> None: |
| if b: |
| self.visit_block(b) |
| |
| def visit_expression_stmt(self, s: ExpressionStmt) -> None: |
| self.statement = s |
| s.expr.accept(self) |
| |
| def visit_return_stmt(self, s: ReturnStmt) -> None: |
| self.statement = s |
| if not self.is_func_scope(): |
| self.fail('"return" outside function', s) |
| if s.expr: |
| s.expr.accept(self) |
| |
| def visit_raise_stmt(self, s: RaiseStmt) -> None: |
| self.statement = s |
| if s.expr: |
| s.expr.accept(self) |
| if s.from_expr: |
| s.from_expr.accept(self) |
| |
| def visit_assert_stmt(self, s: AssertStmt) -> None: |
| self.statement = s |
| if s.expr: |
| s.expr.accept(self) |
| if s.msg: |
| s.msg.accept(self) |
| |
| def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: |
| self.statement = s |
| s.lvalue.accept(self) |
| s.rvalue.accept(self) |
| if ( |
| isinstance(s.lvalue, NameExpr) |
| and s.lvalue.name == "__all__" |
| and s.lvalue.kind == GDEF |
| and isinstance(s.rvalue, (ListExpr, TupleExpr)) |
| ): |
| self.add_exports(s.rvalue.items) |
| |
| def visit_while_stmt(self, s: WhileStmt) -> None: |
| self.statement = s |
| s.expr.accept(self) |
| self.loop_depth[-1] += 1 |
| s.body.accept(self) |
| self.loop_depth[-1] -= 1 |
| self.visit_block_maybe(s.else_body) |
| |
| def visit_for_stmt(self, s: ForStmt) -> None: |
| if s.is_async: |
| if not self.is_func_scope() or not self.function_stack[-1].is_coroutine: |
| self.fail(message_registry.ASYNC_FOR_OUTSIDE_COROUTINE, s, code=codes.SYNTAX) |
| |
| self.statement = s |
| s.expr.accept(self) |
| |
| # Bind index variables and check if they define new names. |
| self.analyze_lvalue(s.index, explicit_type=s.index_type is not None) |
| if s.index_type: |
| if self.is_classvar(s.index_type): |
| self.fail_invalid_classvar(s.index) |
| allow_tuple_literal = isinstance(s.index, TupleExpr) |
| analyzed = self.anal_type(s.index_type, allow_tuple_literal=allow_tuple_literal) |
| if analyzed is not None: |
| self.store_declared_types(s.index, analyzed) |
| s.index_type = analyzed |
| |
| self.loop_depth[-1] += 1 |
| self.visit_block(s.body) |
| self.loop_depth[-1] -= 1 |
| |
| self.visit_block_maybe(s.else_body) |
| |
| def visit_break_stmt(self, s: BreakStmt) -> None: |
| self.statement = s |
| if self.loop_depth[-1] == 0: |
| self.fail('"break" outside loop', s, serious=True, blocker=True) |
| |
| def visit_continue_stmt(self, s: ContinueStmt) -> None: |
| self.statement = s |
| if self.loop_depth[-1] == 0: |
| self.fail('"continue" outside loop', s, serious=True, blocker=True) |
| |
| def visit_if_stmt(self, s: IfStmt) -> None: |
| self.statement = s |
| infer_reachability_of_if_statement(s, self.options) |
| for i in range(len(s.expr)): |
| s.expr[i].accept(self) |
| self.visit_block(s.body[i]) |
| self.visit_block_maybe(s.else_body) |
| |
| def visit_try_stmt(self, s: TryStmt) -> None: |
| self.statement = s |
| self.analyze_try_stmt(s, self) |
| |
| def analyze_try_stmt(self, s: TryStmt, visitor: NodeVisitor[None]) -> None: |
| s.body.accept(visitor) |
| for type, var, handler in zip(s.types, s.vars, s.handlers): |
| if type: |
| type.accept(visitor) |
| if var: |
| self.analyze_lvalue(var) |
| handler.accept(visitor) |
| if s.else_body: |
| s.else_body.accept(visitor) |
| if s.finally_body: |
| s.finally_body.accept(visitor) |
| |
| def visit_with_stmt(self, s: WithStmt) -> None: |
| self.statement = s |
| types: list[Type] = [] |
| |
| if s.is_async: |
| if not self.is_func_scope() or not self.function_stack[-1].is_coroutine: |
| self.fail(message_registry.ASYNC_WITH_OUTSIDE_COROUTINE, s, code=codes.SYNTAX) |
| |
| if s.unanalyzed_type: |
| assert isinstance(s.unanalyzed_type, ProperType) |
| actual_targets = [t for t in s.target if t is not None] |
| if len(actual_targets) == 0: |
| # We have a type for no targets |
| self.fail('Invalid type comment: "with" statement has no targets', s) |
| elif len(actual_targets) == 1: |
| # We have one target and one type |
| types = [s.unanalyzed_type] |
| elif isinstance(s.unanalyzed_type, TupleType): |
| # We have multiple targets and multiple types |
| if len(actual_targets) == len(s.unanalyzed_type.items): |
| types = s.unanalyzed_type.items.copy() |
| else: |
| # But it's the wrong number of items |
| self.fail('Incompatible number of types for "with" targets', s) |
| else: |
| # We have multiple targets and one type |
| self.fail('Multiple types expected for multiple "with" targets', s) |
| |
| new_types: list[Type] = [] |
| for e, n in zip(s.expr, s.target): |
| e.accept(self) |
| if n: |
| self.analyze_lvalue(n, explicit_type=s.unanalyzed_type is not None) |
| |
| # Since we have a target, pop the next type from types |
| if types: |
| t = types.pop(0) |
| if self.is_classvar(t): |
| self.fail_invalid_classvar(n) |
| allow_tuple_literal = isinstance(n, TupleExpr) |
| analyzed = self.anal_type(t, allow_tuple_literal=allow_tuple_literal) |
| if analyzed is not None: |
| # TODO: Deal with this better |
| new_types.append(analyzed) |
| self.store_declared_types(n, analyzed) |
| |
| s.analyzed_types = new_types |
| |
| self.visit_block(s.body) |
| |
| def visit_del_stmt(self, s: DelStmt) -> None: |
| self.statement = s |
| s.expr.accept(self) |
| if not self.is_valid_del_target(s.expr): |
| self.fail("Invalid delete target", s) |
| |
| def is_valid_del_target(self, s: Expression) -> bool: |
| if isinstance(s, (IndexExpr, NameExpr, MemberExpr)): |
| return True |
| elif isinstance(s, (TupleExpr, ListExpr)): |
| return all(self.is_valid_del_target(item) for item in s.items) |
| else: |
| return False |
| |
| def visit_global_decl(self, g: GlobalDecl) -> None: |
| self.statement = g |
| for name in g.names: |
| if name in self.nonlocal_decls[-1]: |
| self.fail(f'Name "{name}" is nonlocal and global', g) |
| self.global_decls[-1].add(name) |
| |
| def visit_nonlocal_decl(self, d: NonlocalDecl) -> None: |
| self.statement = d |
| if self.is_module_scope(): |
| self.fail("nonlocal declaration not allowed at module level", d) |
| else: |
| for name in d.names: |
| for table in reversed(self.locals[:-1]): |
| if table is not None and name in table: |
| break |
| else: |
| self.fail(f'No binding for nonlocal "{name}" found', d) |
| |
| if self.locals[-1] is not None and name in self.locals[-1]: |
| self.fail( |
| 'Name "{}" is already defined in local ' |
| "scope before nonlocal declaration".format(name), |
| d, |
| ) |
| |
| if name in self.global_decls[-1]: |
| self.fail(f'Name "{name}" is nonlocal and global', d) |
| self.nonlocal_decls[-1].add(name) |
| |
| def visit_match_stmt(self, s: MatchStmt) -> None: |
| self.statement = s |
| infer_reachability_of_match_statement(s, self.options) |
| s.subject.accept(self) |
| for i in range(len(s.patterns)): |
| s.patterns[i].accept(self) |
| guard = s.guards[i] |
| if guard is not None: |
| guard.accept(self) |
| self.visit_block(s.bodies[i]) |
| |
| # |
| # Expressions |
| # |
| |
| def visit_name_expr(self, expr: NameExpr) -> None: |
| n = self.lookup(expr.name, expr) |
| if n: |
| self.bind_name_expr(expr, n) |
| |
| def bind_name_expr(self, expr: NameExpr, sym: SymbolTableNode) -> None: |
| """Bind name expression to a symbol table node.""" |
| if isinstance(sym.node, TypeVarExpr) and self.tvar_scope.get_binding(sym): |
| self.fail( |
| '"{}" is a type variable and only valid in type ' "context".format(expr.name), expr |
| ) |
| elif isinstance(sym.node, PlaceholderNode): |
| self.process_placeholder(expr.name, "name", expr) |
| else: |
| expr.kind = sym.kind |
| expr.node = sym.node |
| expr.fullname = sym.fullname or "" |
| |
| def visit_super_expr(self, expr: SuperExpr) -> None: |
| if not self.type and not expr.call.args: |
| self.fail('"super" used outside class', expr) |
| return |
| expr.info = self.type |
| for arg in expr.call.args: |
| arg.accept(self) |
| |
| def visit_tuple_expr(self, expr: TupleExpr) -> None: |
| for item in expr.items: |
| if isinstance(item, StarExpr): |
| item.valid = True |
| item.accept(self) |
| |
| def visit_list_expr(self, expr: ListExpr) -> None: |
| for item in expr.items: |
| if isinstance(item, StarExpr): |
| item.valid = True |
| item.accept(self) |
| |
| def visit_set_expr(self, expr: SetExpr) -> None: |
| for item in expr.items: |
| if isinstance(item, StarExpr): |
| item.valid = True |
| item.accept(self) |
| |
| def visit_dict_expr(self, expr: DictExpr) -> None: |
| for key, value in expr.items: |
| if key is not None: |
| key.accept(self) |
| value.accept(self) |
| |
| def visit_star_expr(self, expr: StarExpr) -> None: |
| if not expr.valid: |
| self.fail("Can use starred expression only as assignment target", expr, blocker=True) |
| else: |
| expr.expr.accept(self) |
| |
| def visit_yield_from_expr(self, e: YieldFromExpr) -> None: |
| if not self.is_func_scope(): |
| self.fail('"yield from" outside function', e, serious=True, blocker=True) |
| elif self.is_comprehension_stack[-1]: |
| self.fail( |
| '"yield from" inside comprehension or generator expression', |
| e, |
| serious=True, |
| blocker=True, |
| ) |
| elif self.function_stack[-1].is_coroutine: |
| self.fail('"yield from" in async function', e, serious=True, blocker=True) |
| else: |
| self.function_stack[-1].is_generator = True |
| if e.expr: |
| e.expr.accept(self) |
| |
| def visit_call_expr(self, expr: CallExpr) -> None: |
| """Analyze a call expression. |
| |
| Some call expressions are recognized as special forms, including |
| cast(...). |
| """ |
| expr.callee.accept(self) |
| if refers_to_fullname(expr.callee, "typing.cast"): |
| # Special form cast(...). |
| if not self.check_fixed_args(expr, 2, "cast"): |
| return |
| # Translate first argument to an unanalyzed type. |
| try: |
| target = self.expr_to_unanalyzed_type(expr.args[0]) |
| except TypeTranslationError: |
| self.fail("Cast target is not a type", expr) |
| return |
| # Piggyback CastExpr object to the CallExpr object; it takes |
| # precedence over the CallExpr semantics. |
| expr.analyzed = CastExpr(expr.args[1], target) |
| expr.analyzed.line = expr.line |
| expr.analyzed.column = expr.column |
| expr.analyzed.accept(self) |
| elif refers_to_fullname(expr.callee, ASSERT_TYPE_NAMES): |
| if not self.check_fixed_args(expr, 2, "assert_type"): |
| return |
| # Translate second argument to an unanalyzed type. |
| try: |
| target = self.expr_to_unanalyzed_type(expr.args[1]) |
| except TypeTranslationError: |
| self.fail("assert_type() type is not a type", expr) |
| return |
| expr.analyzed = AssertTypeExpr(expr.args[0], target) |
| expr.analyzed.line = expr.line |
| expr.analyzed.column = expr.column |
| expr.analyzed.accept(self) |
| elif refers_to_fullname(expr.callee, REVEAL_TYPE_NAMES): |
| if not self.check_fixed_args(expr, 1, "reveal_type"): |
| return |
| expr.analyzed = RevealExpr(kind=REVEAL_TYPE, expr=expr.args[0]) |
| expr.analyzed.line = expr.line |
| expr.analyzed.column = expr.column |
| expr.analyzed.accept(self) |
| elif refers_to_fullname(expr.callee, "builtins.reveal_locals"): |
| # Store the local variable names into the RevealExpr for use in the |
| # type checking pass |
| local_nodes: list[Var] = [] |
| if self.is_module_scope(): |
| # try to determine just the variable declarations in module scope |
| # self.globals.values() contains SymbolTableNode's |
| # Each SymbolTableNode has an attribute node that is nodes.Var |
| # look for variable nodes that marked as is_inferred |
| # Each symboltable node has a Var node as .node |
| local_nodes = [ |
| n.node |
| for name, n in self.globals.items() |
| if getattr(n.node, "is_inferred", False) and isinstance(n.node, Var) |
| ] |
| elif self.is_class_scope(): |
| # type = None # type: Optional[TypeInfo] |
| if self.type is not None: |
| local_nodes = [ |
| st.node for st in self.type.names.values() if isinstance(st.node, Var) |
| ] |
| elif self.is_func_scope(): |
| # locals = None # type: List[Optional[SymbolTable]] |
| if self.locals is not None: |
| symbol_table = self.locals[-1] |
| if symbol_table is not None: |
| local_nodes = [ |
| st.node for st in symbol_table.values() if isinstance(st.node, Var) |
| ] |
| expr.analyzed = RevealExpr(kind=REVEAL_LOCALS, local_nodes=local_nodes) |
| expr.analyzed.line = expr.line |
| expr.analyzed.column = expr.column |
| expr.analyzed.accept(self) |
| elif refers_to_fullname(expr.callee, "typing.Any"): |
| # Special form Any(...) no longer supported. |
| self.fail("Any(...) is no longer supported. Use cast(Any, ...) instead", expr) |
| elif refers_to_fullname(expr.callee, "typing._promote"): |
| # Special form _promote(...). |
| if not self.check_fixed_args(expr, 1, "_promote"): |
| return |
| # Translate first argument to an unanalyzed type. |
| try: |
| target = self.expr_to_unanalyzed_type(expr.args[0]) |
| except TypeTranslationError: |
| self.fail("Argument 1 to _promote is not a type", expr) |
| return |
| expr.analyzed = PromoteExpr(target) |
| expr.analyzed.line = expr.line |
| expr.analyzed.accept(self) |
| elif refers_to_fullname(expr.callee, "builtins.dict"): |
| expr.analyzed = self.translate_dict_call(expr) |
| elif refers_to_fullname(expr.callee, "builtins.divmod"): |
| if not self.check_fixed_args(expr, 2, "divmod"): |
| return |
| expr.analyzed = OpExpr("divmod", expr.args[0], expr.args[1]) |
| expr.analyzed.line = expr.line |
| expr.analyzed.accept(self) |
| else: |
| # Normal call expression. |
| for a in expr.args: |
| a.accept(self) |
| |
| if ( |
| isinstance(expr.callee, MemberExpr) |
| and isinstance(expr.callee.expr, NameExpr) |
| and expr.callee.expr.name == "__all__" |
| and expr.callee.expr.kind == GDEF |
| and expr.callee.name in ("append", "extend", "remove") |
| ): |
| if expr.callee.name == "append" and expr.args: |
| self.add_exports(expr.args[0]) |
| elif ( |
| expr.callee.name == "extend" |
| and expr.args |
| and isinstance(expr.args[0], (ListExpr, TupleExpr)) |
| ): |
| self.add_exports(expr.args[0].items) |
| elif ( |
| expr.callee.name == "remove" |
| and expr.args |
| and isinstance(expr.args[0], StrExpr) |
| ): |
| self.all_exports = [n for n in self.all_exports if n != expr.args[0].value] |
| |
| def translate_dict_call(self, call: CallExpr) -> DictExpr | None: |
| """Translate 'dict(x=y, ...)' to {'x': y, ...} and 'dict()' to {}. |
| |
| For other variants of dict(...), return None. |
| """ |
| if not all(kind in (ARG_NAMED, ARG_STAR2) for kind in call.arg_kinds): |
| # Must still accept those args. |
| for a in call.args: |
| a.accept(self) |
| return None |
| expr = DictExpr( |
| [ |
| (StrExpr(key) if key is not None else None, value) |
| for key, value in zip(call.arg_names, call.args) |
| ] |
| ) |
| expr.set_line(call) |
| expr.accept(self) |
| return expr |
| |
| def check_fixed_args(self, expr: CallExpr, numargs: int, name: str) -> bool: |
| """Verify that expr has specified number of positional args. |
| |
| Return True if the arguments are valid. |
| """ |
| s = "s" |
| if numargs == 1: |
| s = "" |
| if len(expr.args) != numargs: |
| self.fail('"%s" expects %d argument%s' % (name, numargs, s), expr) |
| return False |
| if expr.arg_kinds != [ARG_POS] * numargs: |
| self.fail(f'"{name}" must be called with {numargs} positional argument{s}', expr) |
| return False |
| return True |
| |
| def visit_member_expr(self, expr: MemberExpr) -> None: |
| base = expr.expr |
| base.accept(self) |
| if isinstance(base, RefExpr) and isinstance(base.node, MypyFile): |
| # Handle module attribute. |
| sym = self.get_module_symbol(base.node, expr.name) |
| if sym: |
| if isinstance(sym.node, PlaceholderNode): |
| self.process_placeholder(expr.name, "attribute", expr) |
| return |
| expr.kind = sym.kind |
| expr.fullname = sym.fullname or "" |
| expr.node = sym.node |
| elif isinstance(base, RefExpr): |
| # This branch handles the case C.bar (or cls.bar or self.bar inside |
| # a classmethod/method), where C is a class and bar is a type |
| # definition or a module resulting from `import bar` (or a module |
| # assignment) inside class C. We look up bar in the class' TypeInfo |
| # namespace. This is done only when bar is a module or a type; |
| # other things (e.g. methods) are handled by other code in |
| # checkmember. |
| type_info = None |
| if isinstance(base.node, TypeInfo): |
| # C.bar where C is a class |
| type_info = base.node |
| elif isinstance(base.node, Var) and self.type and self.function_stack: |
| # check for self.bar or cls.bar in method/classmethod |
| func_def = self.function_stack[-1] |
| if not func_def.is_static and isinstance(func_def.type, CallableType): |
| formal_arg = func_def.type.argument_by_name(base.node.name) |
| if formal_arg and formal_arg.pos == 0: |
| type_info = self.type |
| elif isinstance(base.node, TypeAlias) and base.node.no_args: |
| assert isinstance(base.node.target, ProperType) |
| if isinstance(base.node.target, Instance): |
| type_info = base.node.target.type |
| |
| if type_info: |
| n = type_info.names.get(expr.name) |
| if n is not None and isinstance(n.node, (MypyFile, TypeInfo, TypeAlias)): |
| if not n: |
| return |
| expr.kind = n.kind |
| expr.fullname = n.fullname or "" |
| expr.node = n.node |
| |
| def visit_op_expr(self, expr: OpExpr) -> None: |
| expr.left.accept(self) |
| |
| if expr.op in ("and", "or"): |
| inferred = infer_condition_value(expr.left, self.options) |
| if (inferred in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "and") or ( |
| inferred in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "or" |
| ): |
| expr.right_unreachable = True |
| return |
| elif (inferred in (ALWAYS_TRUE, MYPY_TRUE) and expr.op == "and") or ( |
| inferred in (ALWAYS_FALSE, MYPY_FALSE) and expr.op == "or" |
| ): |
| expr.right_always = True |
| |
| expr.right.accept(self) |
| |
| def visit_comparison_expr(self, expr: ComparisonExpr) -> None: |
| for operand in expr.operands: |
| operand.accept(self) |
| |
| def visit_unary_expr(self, expr: UnaryExpr) -> None: |
| expr.expr.accept(self) |
| |
| def visit_index_expr(self, expr: IndexExpr) -> None: |
| base = expr.base |
| base.accept(self) |
| if ( |
| isinstance(base, RefExpr) |
| and isinstance(base.node, TypeInfo) |
| and not base.node.is_generic() |
| ): |
| expr.index.accept(self) |
| elif ( |
| isinstance(base, RefExpr) and isinstance(base.node, TypeAlias) |
| ) or refers_to_class_or_function(base): |
| # We need to do full processing on every iteration, since some type |
| # arguments may contain placeholder types. |
| self.analyze_type_application(expr) |
| else: |
| expr.index.accept(self) |
| |
| def analyze_type_application(self, expr: IndexExpr) -> None: |
| """Analyze special form -- type application (either direct or via type aliasing).""" |
| types = self.analyze_type_application_args(expr) |
| if types is None: |
| return |
| base = expr.base |
| expr.analyzed = TypeApplication(base, types) |
| expr.analyzed.line = expr.line |
| expr.analyzed.column = expr.column |
| # Types list, dict, set are not subscriptable, prohibit this if |
| # subscripted either via type alias... |
| if isinstance(base, RefExpr) and isinstance(base.node, TypeAlias): |
| alias = base.node |
| target = get_proper_type(alias.target) |
| if isinstance(target, Instance): |
| name = target.type.fullname |
| if ( |
| alias.no_args |
| and name # this avoids bogus errors for already reported aliases |
| in get_nongen_builtins(self.options.python_version) |
| and not self.is_stub_file |
| and not alias.normalized |
| ): |
| self.fail(no_subscript_builtin_alias(name, propose_alt=False), expr) |
| # ...or directly. |
| else: |
| n = self.lookup_type_node(base) |
| if ( |
| n |
| and n.fullname in get_nongen_builtins(self.options.python_version) |
| and not self.is_stub_file |
| ): |
| self.fail(no_subscript_builtin_alias(n.fullname, propose_alt=False), expr) |
| |
| def analyze_type_application_args(self, expr: IndexExpr) -> list[Type] | None: |
| """Analyze type arguments (index) in a type application. |
| |
| Return None if anything was incomplete. |
| """ |
| index = expr.index |
| tag = self.track_incomplete_refs() |
| self.analyze_type_expr(index) |
| if self.found_incomplete_ref(tag): |
| return None |
| if self.basic_type_applications: |
| # Postpone the rest until we have more information (for r.h.s. of an assignment) |
| return None |
| types: list[Type] = [] |
| if isinstance(index, TupleExpr): |
| items = index.items |
| is_tuple = isinstance(expr.base, RefExpr) and expr.base.fullname == "builtins.tuple" |
| if is_tuple and len(items) == 2 and isinstance(items[-1], EllipsisExpr): |
| items = items[:-1] |
| else: |
| items = [index] |
| |
| # whether param spec literals be allowed here |
| # TODO: should this be computed once and passed in? |
| # or is there a better way to do this? |
| base = expr.base |
| if isinstance(base, RefExpr) and isinstance(base.node, TypeAlias): |
| alias = base.node |
| target = get_proper_type(alias.target) |
| if isinstance(target, Instance): |
| has_param_spec = target.type.has_param_spec_type |
| num_args = len(target.type.type_vars) |
| else: |
| has_param_spec = False |
| num_args = -1 |
| elif isinstance(base, NameExpr) and isinstance(base.node, TypeInfo): |
| has_param_spec = base.node.has_param_spec_type |
| num_args = len(base.node.type_vars) |
| else: |
| has_param_spec = False |
| num_args = -1 |
| |
| for item in items: |
| try: |
| typearg = self.expr_to_unanalyzed_type(item) |
| except TypeTranslationError: |
| self.fail("Type expected within [...]", expr) |
| return None |
| analyzed = self.anal_type( |
| typearg, |
| # The type application may appear in base class expression, |
| # where type variables are not bound yet. Or when accepting |
| # r.h.s. of type alias before we figured out it is a type alias. |
| allow_unbound_tvars=self.allow_unbound_tvars, |
| allow_placeholder=True, |
| allow_param_spec_literals=has_param_spec, |
| ) |
| if analyzed is None: |
| return None |
| types.append(analyzed) |
| |
| if has_param_spec and num_args == 1 and types: |
| first_arg = get_proper_type(types[0]) |
| if not ( |
| len(types) == 1 and isinstance(first_arg, (Parameters, ParamSpecType, AnyType)) |
| ): |
| types = [Parameters(types, [ARG_POS] * len(types), [None] * len(types))] |
| |
| return types |
| |
| def visit_slice_expr(self, expr: SliceExpr) -> None: |
| if expr.begin_index: |
| expr.begin_index.accept(self) |
| if expr.end_index: |
| expr.end_index.accept(self) |
| if expr.stride: |
| expr.stride.accept(self) |
| |
| def visit_cast_expr(self, expr: CastExpr) -> None: |
| expr.expr.accept(self) |
| analyzed = self.anal_type(expr.type) |
| if analyzed is not None: |
| expr.type = analyzed |
| |
| def visit_assert_type_expr(self, expr: AssertTypeExpr) -> None: |
| expr.expr.accept(self) |
| analyzed = self.anal_type(expr.type) |
| if analyzed is not None: |
| expr.type = analyzed |
| |
| def visit_reveal_expr(self, expr: RevealExpr) -> None: |
| if expr.kind == REVEAL_TYPE: |
| if expr.expr is not None: |
| expr.expr.accept(self) |
| else: |
| # Reveal locals doesn't have an inner expression, there's no |
| # need to traverse inside it |
| pass |
| |
| def visit_type_application(self, expr: TypeApplication) -> None: |
| expr.expr.accept(self) |
| for i in range(len(expr.types)): |
| analyzed = self.anal_type(expr.types[i]) |
| if analyzed is not None: |
| expr.types[i] = analyzed |
| |
| def visit_list_comprehension(self, expr: ListComprehension) -> None: |
| if any(expr.generator.is_async): |
| if not self.is_func_scope() or not self.function_stack[-1].is_coroutine: |
| self.fail(message_registry.ASYNC_FOR_OUTSIDE_COROUTINE, expr, code=codes.SYNTAX) |
| |
| expr.generator.accept(self) |
| |
| def visit_set_comprehension(self, expr: SetComprehension) -> None: |
| if any(expr.generator.is_async): |
| if not self.is_func_scope() or not self.function_stack[-1].is_coroutine: |
| self.fail(message_registry.ASYNC_FOR_OUTSIDE_COROUTINE, expr, code=codes.SYNTAX) |
| |
| expr.generator.accept(self) |
| |
| def visit_dictionary_comprehension(self, expr: DictionaryComprehension) -> None: |
| if any(expr.is_async): |
| if not self.is_func_scope() or not self.function_stack[-1].is_coroutine: |
| self.fail(message_registry.ASYNC_FOR_OUTSIDE_COROUTINE, expr, code=codes.SYNTAX) |
| |
| with self.enter(expr): |
| self.analyze_comp_for(expr) |
| expr.key.accept(self) |
| expr.value.accept(self) |
| self.analyze_comp_for_2(expr) |
| |
| def visit_generator_expr(self, expr: GeneratorExpr) -> None: |
| with self.enter(expr): |
| self.analyze_comp_for(expr) |
| expr.left_expr.accept(self) |
| self.analyze_comp_for_2(expr) |
| |
| def analyze_comp_for(self, expr: GeneratorExpr | DictionaryComprehension) -> None: |
| """Analyses the 'comp_for' part of comprehensions (part 1). |
| |
| That is the part after 'for' in (x for x in l if p). This analyzes |
| variables and conditions which are analyzed in a local scope. |
| """ |
| for i, (index, sequence, conditions) in enumerate( |
| zip(expr.indices, expr.sequences, expr.condlists) |
| ): |
| if i > 0: |
| sequence.accept(self) |
| # Bind index variables. |
| self.analyze_lvalue(index) |
| for cond in conditions: |
| cond.accept(self) |
| |
| def analyze_comp_for_2(self, expr: GeneratorExpr | DictionaryComprehension) -> None: |
| """Analyses the 'comp_for' part of comprehensions (part 2). |
| |
| That is the part after 'for' in (x for x in l if p). This analyzes |
| the 'l' part which is analyzed in the surrounding scope. |
| """ |
| expr.sequences[0].accept(self) |
| |
| def visit_lambda_expr(self, expr: LambdaExpr) -> None: |
| self.analyze_arg_initializers(expr) |
| self.analyze_function_body(expr) |
| |
| def visit_conditional_expr(self, expr: ConditionalExpr) -> None: |
| expr.if_expr.accept(self) |
| expr.cond.accept(self) |
| expr.else_expr.accept(self) |
| |
| def visit__promote_expr(self, expr: PromoteExpr) -> None: |
| analyzed = self.anal_type(expr.type) |
| if analyzed is not None: |
| assert isinstance(analyzed, ProperType), "Cannot use type aliases for promotions" |
| expr.type = analyzed |
| |
| def visit_yield_expr(self, e: YieldExpr) -> None: |
| if not self.is_func_scope(): |
| self.fail('"yield" outside function', e, serious=True, blocker=True) |
| elif self.is_comprehension_stack[-1]: |
| self.fail( |
| '"yield" inside comprehension or generator expression', |
| e, |
| serious=True, |
| blocker=True, |
| ) |
| elif self.function_stack[-1].is_coroutine: |
| self.function_stack[-1].is_generator = True |
| self.function_stack[-1].is_async_generator = True |
| else: |
| self.function_stack[-1].is_generator = True |
| if e.expr: |
| e.expr.accept(self) |
| |
| def visit_await_expr(self, expr: AwaitExpr) -> None: |
| if not self.is_func_scope() or not self.function_stack: |
| # We check both because is_function_scope() returns True inside comprehensions. |
| # This is not a blocker, because some enviroments (like ipython) |
| # support top level awaits. |
| self.fail('"await" outside function', expr, serious=True, code=codes.TOP_LEVEL_AWAIT) |
| elif not self.function_stack[-1].is_coroutine: |
| self.fail('"await" outside coroutine ("async def")', expr, serious=True, blocker=True) |
| expr.expr.accept(self) |
| |
| # |
| # Patterns |
| # |
| |
| def visit_as_pattern(self, p: AsPattern) -> None: |
| if p.pattern is not None: |
| p.pattern.accept(self) |
| if p.name is not None: |
| self.analyze_lvalue(p.name) |
| |
| def visit_or_pattern(self, p: OrPattern) -> None: |
| for pattern in p.patterns: |
| pattern.accept(self) |
| |
| def visit_value_pattern(self, p: ValuePattern) -> None: |
| p.expr.accept(self) |
| |
| def visit_sequence_pattern(self, p: SequencePattern) -> None: |
| for pattern in p.patterns: |
| pattern.accept(self) |
| |
| def visit_starred_pattern(self, p: StarredPattern) -> None: |
| if p.capture is not None: |
| self.analyze_lvalue(p.capture) |
| |
| def visit_mapping_pattern(self, p: MappingPattern) -> None: |
| for key in p.keys: |
| key.accept(self) |
| for value in p.values: |
| value.accept(self) |
| if p.rest is not None: |
| self.analyze_lvalue(p.rest) |
| |
| def visit_class_pattern(self, p: ClassPattern) -> None: |
| p.class_ref.accept(self) |
| for pos in p.positionals: |
| pos.accept(self) |
| for v in p.keyword_values: |
| v.accept(self) |
| |
| # |
| # Lookup functions |
| # |
| |
| def lookup( |
| self, name: str, ctx: Context, suppress_errors: bool = False |
| ) -> SymbolTableNode | None: |
| """Look up an unqualified (no dots) name in all active namespaces. |
| |
| Note that the result may contain a PlaceholderNode. The caller may |
| want to defer in that case. |
| |
| Generate an error if the name is not defined unless suppress_errors |
| is true or the current namespace is incomplete. In the latter case |
| defer. |
| """ |
| implicit_name = False |
| # 1a. Name declared using 'global x' takes precedence |
| if name in self.global_decls[-1]: |
| if name in self.globals: |
| return self.globals[name] |
| if not suppress_errors: |
| self.name_not_defined(name, ctx) |
| return None |
| # 1b. Name declared using 'nonlocal x' takes precedence |
| if name in self.nonlocal_decls[-1]: |
| for table in reversed(self.locals[:-1]): |
| if table is not None and name in table: |
| return table[name] |
| if not suppress_errors: |
| self.name_not_defined(name, ctx) |
| return None |
| # 2. Class attributes (if within class definition) |
| if self.type and not self.is_func_scope() and name in self.type.names: |
| node = self.type.names[name] |
| if not node.implicit: |
| if self.is_active_symbol_in_class_body(node.node): |
| return node |
| else: |
| # Defined through self.x assignment |
| implicit_name = True |
| implicit_node = node |
| # 3. Local (function) scopes |
| for table in reversed(self.locals): |
| if table is not None and name in table: |
| return table[name] |
| # 4. Current file global scope |
| if name in self.globals: |
| return self.globals[name] |
| # 5. Builtins |
| b = self.globals.get("__builtins__", None) |
| if b: |
| assert isinstance(b.node, MypyFile) |
| table = b.node.names |
| if name in table: |
| if len(name) > 1 and name[0] == "_" and name[1] != "_": |
| if not suppress_errors: |
| self.name_not_defined(name, ctx) |
| return None |
| node = table[name] |
| return node |
| # Give up. |
| if not implicit_name and not suppress_errors: |
| self.name_not_defined(name, ctx) |
| else: |
| if implicit_name: |
| return implicit_node |
| return None |
| |
| def is_active_symbol_in_class_body(self, node: SymbolNode | None) -> bool: |
| """Can a symbol defined in class body accessed at current statement? |
| |
| Only allow access to class attributes textually after |
| the definition, so that it's possible to fall back to the |
| outer scope. Example: |
| |
| class X: ... |
| |
| class C: |
| X = X # Initializer refers to outer scope |
| |
| Nested classes are an exception, since we want to support |
| arbitrary forward references in type annotations. Also, we |
| allow forward references to type aliases to support recursive |
| types. |
| """ |
| # TODO: Forward reference to name imported in class body is not |
| # caught. |
| if self.statement is None: |
| # Assume it's fine -- don't have enough context to check |
| return True |
| return ( |
| node is None |
| or self.is_textually_before_statement(node) |
| or not self.is_defined_in_current_module(node.fullname) |
| or isinstance(node, (TypeInfo, TypeAlias)) |
| or (isinstance(node, PlaceholderNode) and node.becomes_typeinfo) |
| ) |
| |
| def is_textually_before_statement(self, node: SymbolNode) -> bool: |
| """Check if a node is defined textually before the current statement |
| |
| Note that decorated functions' line number are the same as |
| the top decorator. |
| """ |
| assert self.statement |
| line_diff = self.statement.line - node.line |
| |
| # The first branch handles reference an overloaded function variant inside itself, |
| # this is a corner case where mypy technically deviates from runtime name resolution, |
| # but it is fine because we want an overloaded function to be treated as a single unit. |
| if self.is_overloaded_item(node, self.statement): |
| return False |
| elif isinstance(node, Decorator) and not node.is_overload: |
| return line_diff > len(node.original_decorators) |
| else: |
| return line_diff > 0 |
| |
| def is_overloaded_item(self, node: SymbolNode, statement: Statement) -> bool: |
| """Check whether the function belongs to the overloaded variants""" |
| if isinstance(node, OverloadedFuncDef) and isinstance(statement, FuncDef): |
| in_items = statement in { |
| item.func if isinstance(item, Decorator) else item for item in node.items |
| } |
| in_impl = node.impl is not None and ( |
| (isinstance(node.impl, Decorator) and statement is node.impl.func) |
| or statement is node.impl |
| ) |
| return in_items or in_impl |
| return False |
| |
| def is_defined_in_current_module(self, fullname: str | None) -> bool: |
| if not fullname: |
| return False |
| return module_prefix(self.modules, fullname) == self.cur_mod_id |
| |
| def lookup_qualified( |
| self, name: str, ctx: Context, suppress_errors: bool = False |
| ) -> SymbolTableNode | None: |
| """Lookup a qualified name in all activate namespaces. |
| |
| Note that the result may contain a PlaceholderNode. The caller may |
| want to defer in that case. |
| |
| Generate an error if the name is not defined unless suppress_errors |
| is true or the current namespace is incomplete. In the latter case |
| defer. |
| """ |
| if "." not in name: |
| # Simple case: look up a short name. |
| return self.lookup(name, ctx, suppress_errors=suppress_errors) |
| parts = name.split(".") |
| namespace = self.cur_mod_id |
| sym = self.lookup(parts[0], ctx, suppress_errors=suppress_errors) |
| if sym: |
| for i in range(1, len(parts)): |
| node = sym.node |
| part = parts[i] |
| if isinstance(node, TypeInfo): |
| nextsym = node.get(part) |
| elif isinstance(node, MypyFile): |
| nextsym = self.get_module_symbol(node, part) |
| namespace = node.fullname |
| elif isinstance(node, PlaceholderNode): |
| return sym |
| elif isinstance(node, TypeAlias) and node.no_args: |
| assert isinstance(node.target, ProperType) |
| if isinstance(node.target, Instance): |
| nextsym = node.target.type.get(part) |
| else: |
| nextsym = None |
| else: |
| if isinstance(node, Var): |
| typ = get_proper_type(node.type) |
| if isinstance(typ, AnyType): |
| # Allow access through Var with Any type without error. |
| return self.implicit_symbol(sym, name, parts[i:], typ) |
| # This might be something like valid `P.args` or invalid `P.__bound__` access. |
| # Important note that `ParamSpecExpr` is also ignored in other places. |
| # See https://github.com/python/mypy/pull/13468 |
| if isinstance(node, ParamSpecExpr) and part in ("args", "kwargs"): |
| return None |
| # Lookup through invalid node, such as variable or function |
| nextsym = None |
| if not nextsym or nextsym.module_hidden: |
| if not suppress_errors: |
| self.name_not_defined(name, ctx, namespace=namespace) |
| return None |
| sym = nextsym |
| return sym |
| |
| def lookup_type_node(self, expr: Expression) -> SymbolTableNode | None: |
| try: |
| t = self.expr_to_unanalyzed_type(expr) |
| except TypeTranslationError: |
| return None |
| if isinstance(t, UnboundType): |
| n = self.lookup_qualified(t.name, expr, suppress_errors=True) |
| return n |
| return None |
| |
| def get_module_symbol(self, node: MypyFile, name: str) -> SymbolTableNode | None: |
| """Look up a symbol from a module. |
| |
| Return None if no matching symbol could be bound. |
| """ |
| module = node.fullname |
| names = node.names |
| sym = names.get(name) |
| if not sym: |
| fullname = module + "." + name |
| if fullname in self.modules: |
| sym = SymbolTableNode(GDEF, self.modules[fullname]) |
| elif self.is_incomplete_namespace(module): |
| self.record_incomplete_ref() |
| elif "__getattr__" in names: |
| gvar = self.create_getattr_var(names["__getattr__"], name, fullname) |
| if gvar: |
| sym = SymbolTableNode(GDEF, gvar) |
| elif self.is_missing_module(fullname): |
| # We use the fullname of the original definition so that we can |
| # detect whether two names refer to the same thing. |
| var_type = AnyType(TypeOfAny.from_unimported_type) |
| v = Var(name, type=var_type) |
| v._fullname = fullname |
| sym = SymbolTableNode(GDEF, v) |
| elif sym.module_hidden: |
| sym = None |
| return sym |
| |
| def is_missing_module(self, module: str) -> bool: |
| return module in self.missing_modules |
| |
| def implicit_symbol( |
| self, sym: SymbolTableNode, name: str, parts: list[str], source_type: AnyType |
| ) -> SymbolTableNode: |
| """Create symbol for a qualified name reference through Any type.""" |
| if sym.node is None: |
| basename = None |
| else: |
| basename = sym.node.fullname |
| if basename is None: |
| fullname = name |
| else: |
| fullname = basename + "." + ".".join(parts) |
| var_type = AnyType(TypeOfAny.from_another_any, source_type) |
| var = Var(parts[-1], var_type) |
| var._fullname = fullname |
| return SymbolTableNode(GDEF, var) |
| |
| def create_getattr_var( |
| self, getattr_defn: SymbolTableNode, name: str, fullname: str |
| ) -> Var | None: |
| """Create a dummy variable using module-level __getattr__ return type. |
| |
| If not possible, return None. |
| |
| Note that multiple Var nodes can be created for a single name. We |
| can use the from_module_getattr and the fullname attributes to |
| check if two dummy Var nodes refer to the same thing. Reusing Var |
| nodes would require non-local mutable state, which we prefer to |
| avoid. |
| """ |
| if isinstance(getattr_defn.node, (FuncDef, Var)): |
| node_type = get_proper_type(getattr_defn.node.type) |
| if isinstance(node_type, CallableType): |
| typ = node_type.ret_type |
| else: |
| typ = AnyType(TypeOfAny.from_error) |
| v = Var(name, type=typ) |
| v._fullname = fullname |
| v.from_module_getattr = True |
| return v |
| return None |
| |
| def lookup_fully_qualified(self, fullname: str) -> SymbolTableNode: |
| ret = self.lookup_fully_qualified_or_none(fullname) |
| assert ret is not None, fullname |
| return ret |
| |
| def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | None: |
| """Lookup a fully qualified name that refers to a module-level definition. |
| |
| Don't assume that the name is defined. This happens in the global namespace -- |
| the local module namespace is ignored. This does not dereference indirect |
| refs. |
| |
| Note that this can't be used for names nested in class namespaces. |
| """ |
| # TODO: unify/clean-up/simplify lookup methods, see #4157. |
| # TODO: support nested classes (but consider performance impact, |
| # we might keep the module level only lookup for thing like 'builtins.int'). |
| assert "." in fullname |
| module, name = fullname.rsplit(".", maxsplit=1) |
| if module not in self.modules: |
| return None |
| filenode = self.modules[module] |
| result = filenode.names.get(name) |
| if result is None and self.is_incomplete_namespace(module): |
| # TODO: More explicit handling of incomplete refs? |
| self.record_incomplete_ref() |
| return result |
| |
| def object_type(self) -> Instance: |
| return self.named_type("builtins.object") |
| |
| def str_type(self) -> Instance: |
| return self.named_type("builtins.str") |
| |
| def named_type(self, fullname: str, args: list[Type] | None = None) -> Instance: |
| sym = self.lookup_fully_qualified(fullname) |
| assert sym, "Internal error: attempted to construct unknown type" |
| node = sym.node |
| assert isinstance(node, TypeInfo) |
| if args: |
| # TODO: assert len(args) == len(node.defn.type_vars) |
| return Instance(node, args) |
| return Instance(node, [AnyType(TypeOfAny.special_form)] * len(node.defn.type_vars)) |
| |
| def named_type_or_none(self, fullname: str, args: list[Type] | None = None) -> Instance | None: |
| sym = self.lookup_fully_qualified_or_none(fullname) |
| if not sym or isinstance(sym.node, PlaceholderNode): |
| return None |
| node = sym.node |
| if isinstance(node, TypeAlias): |
| assert isinstance(node.target, Instance) # type: ignore[misc] |
| node = node.target.type |
| assert isinstance(node, TypeInfo), node |
| if args is not None: |
| # TODO: assert len(args) == len(node.defn.type_vars) |
| return Instance(node, args) |
| return Instance(node, [AnyType(TypeOfAny.unannotated)] * len(node.defn.type_vars)) |
| |
| def builtin_type(self, fully_qualified_name: str) -> Instance: |
| """Legacy function -- use named_type() instead.""" |
| return self.named_type(fully_qualified_name) |
| |
| def lookup_current_scope(self, name: str) -> SymbolTableNode | None: |
| if self.locals[-1] is not None: |
| return self.locals[-1].get(name) |
| elif self.type is not None: |
| return self.type.names.get(name) |
| else: |
| return self.globals.get(name) |
| |
| # |
| # Adding symbols |
| # |
| |
| def add_symbol( |
| self, |
| name: str, |
| node: SymbolNode, |
| context: Context, |
| module_public: bool = True, |
| module_hidden: bool = False, |
| can_defer: bool = True, |
| escape_comprehensions: bool = False, |
| ) -> bool: |
| """Add symbol to the currently active symbol table. |
| |
| Generally additions to symbol table should go through this method or |
| one of the methods below so that kinds, redefinitions, conditional |
| definitions, and skipped names are handled consistently. |
| |
| Return True if we actually added the symbol, or False if we refused to do so |
| (because something is not ready). |
| |
| If can_defer is True, defer current target if adding a placeholder. |
| """ |
| if self.is_func_scope(): |
| kind = LDEF |
| elif self.type is not None: |
| kind = MDEF |
| else: |
| kind = GDEF |
| symbol = SymbolTableNode( |
| kind, node, module_public=module_public, module_hidden=module_hidden |
| ) |
| return self.add_symbol_table_node(name, symbol, context, can_defer, escape_comprehensions) |
| |
| def add_symbol_skip_local(self, name: str, node: SymbolNode) -> None: |
| """Same as above, but skipping the local namespace. |
| |
| This doesn't check for previous definition and is only used |
| for serialization of method-level classes. |
| |
| Classes defined within methods can be exposed through an |
| attribute type, but method-level symbol tables aren't serialized. |
| This method can be used to add such classes to an enclosing, |
| serialized symbol table. |
| """ |
| # TODO: currently this is only used by named tuples and typed dicts. |
| # Use this method also by normal classes, see issue #6422. |
| if self.type is not None: |
| names = self.type.names |
| kind = MDEF |
| else: |
| names = self.globals |
| kind = GDEF |
| symbol = SymbolTableNode(kind, node) |
| names[name] = symbol |
| |
| def add_symbol_table_node( |
| self, |
| name: str, |
| symbol: SymbolTableNode, |
| context: Context | None = None, |
| can_defer: bool = True, |
| escape_comprehensions: bool = False, |
| ) -> bool: |
| """Add symbol table node to the currently active symbol table. |
| |
| Return True if we actually added the symbol, or False if we refused |
| to do so (because something is not ready or it was a no-op). |
| |
| Generate an error if there is an invalid redefinition. |
| |
| If context is None, unconditionally add node, since we can't report |
| an error. Note that this is used by plugins to forcibly replace nodes! |
| |
| TODO: Prevent plugins from replacing nodes, as it could cause problems? |
| |
| Args: |
| name: short name of symbol |
| symbol: Node to add |
| can_defer: if True, defer current target if adding a placeholder |
| context: error context (see above about None value) |
| """ |
| names = self.current_symbol_table(escape_comprehensions=escape_comprehensions) |
| existing = names.get(name) |
| if isinstance(symbol.node, PlaceholderNode) and can_defer: |
| if context is not None: |
| self.process_placeholder(name, "name", context) |
| else: |
| # see note in docstring describing None contexts |
| self.defer() |
| if ( |
| existing is not None |
| and context is not None |
| and not is_valid_replacement(existing, symbol) |
| ): |
| # There is an existing node, so this may be a redefinition. |
| # If the new node points to the same node as the old one, |
| # or if both old and new nodes are placeholders, we don't |
| # need to do anything. |
| old = existing.node |
| new = symbol.node |
| if isinstance(new, PlaceholderNode): |
| # We don't know whether this is okay. Let's wait until the next iteration. |
| return False |
| if not is_same_symbol(old, new): |
| if isinstance(new, (FuncDef, Decorator, OverloadedFuncDef, TypeInfo)): |
| self.add_redefinition(names, name, symbol) |
| if not (isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new)): |
| self.name_already_defined(name, context, existing) |
| elif name not in self.missing_names[-1] and "*" not in self.missing_names[-1]: |
| names[name] = symbol |
| self.progress = True |
| return True |
| return False |
| |
| def add_redefinition(self, names: SymbolTable, name: str, symbol: SymbolTableNode) -> None: |
| """Add a symbol table node that reflects a redefinition as a function or a class. |
| |
| Redefinitions need to be added to the symbol table so that they can be found |
| through AST traversal, but they have dummy names of form 'name-redefinition[N]', |
| where N ranges over 2, 3, ... (omitted for the first redefinition). |
| |
| Note: we always store redefinitions independently of whether they are valid or not |
| (so they will be semantically analyzed), the caller should give an error for invalid |
| redefinitions (such as e.g. variable redefined as a class). |
| """ |
| i = 1 |
| # Don't serialize redefined nodes. They are likely to have |
| # busted internal references which can cause problems with |
| # serialization and they can't have any external references to |
| # them. |
| symbol.no_serialize = True |
| while True: |
| if i == 1: |
| new_name = f"{name}-redefinition" |
| else: |
| new_name = f"{name}-redefinition{i}" |
| existing = names.get(new_name) |
| if existing is None: |
| names[new_name] = symbol |
| return |
| elif existing.node is symbol.node: |
| # Already there |
| return |
| i += 1 |
| |
| def add_local(self, node: Var | FuncDef | OverloadedFuncDef, context: Context) -> None: |
| """Add local variable or function.""" |
| assert self.is_func_scope() |
| name = node.name |
| node._fullname = name |
| self.add_symbol(name, node, context) |
| |
| def _get_node_for_class_scoped_import( |
| self, name: str, symbol_node: SymbolNode | None, context: Context |
| ) -> SymbolNode | None: |
| if symbol_node is None: |
| return None |
| # I promise this type checks; I'm just making mypyc issues go away. |
| # mypyc is absolutely convinced that `symbol_node` narrows to a Var in the following, |
| # when it can also be a FuncBase. Once fixed, `f` in the following can be removed. |
| # See also https://github.com/mypyc/mypyc/issues/892 |
| f: Callable[[object], Any] = lambda x: x |
| if isinstance(f(symbol_node), (Decorator, FuncBase, Var)): |
| # For imports in class scope, we construct a new node to represent the symbol and |
| # set its `info` attribute to `self.type`. |
| existing = self.current_symbol_table().get(name) |
| if ( |
| # The redefinition checks in `add_symbol_table_node` don't work for our |
| # constructed Var / FuncBase, so check for possible redefinitions here. |
| existing is not None |
| and isinstance(f(existing.node), (Decorator, FuncBase, Var)) |
| and ( |
| isinstance(f(existing.type), f(AnyType)) |
| or f(existing.type) == f(symbol_node).type |
| ) |
| ): |
| return existing.node |
| |
| # Construct the new node |
| if isinstance(f(symbol_node), (FuncBase, Decorator)): |
| # In theory we could construct a new node here as well, but in practice |
| # it doesn't work well, see #12197 |
| typ: Type | None = AnyType(TypeOfAny.from_error) |
| self.fail("Unsupported class scoped import", context) |
| else: |
| typ = f(symbol_node).type |
| symbol_node = Var(name, typ) |
| symbol_node._fullname = self.qualified_name(name) |
| assert self.type is not None # guaranteed by is_class_scope |
| symbol_node.info = self.type |
| symbol_node.line = context.line |
| symbol_node.column = context.column |
| return symbol_node |
| |
| def add_imported_symbol( |
| self, |
| name: str, |
| node: SymbolTableNode, |
| context: ImportBase, |
| module_public: bool, |
| module_hidden: bool, |
| ) -> None: |
| """Add an alias to an existing symbol through import.""" |
| assert not module_hidden or not module_public |
| |
| existing_symbol = self.lookup_current_scope(name) |
| if ( |
| existing_symbol |
| and not isinstance(existing_symbol.node, PlaceholderNode) |
| and not isinstance(node.node, PlaceholderNode) |
| ): |
| # Import can redefine a variable. They get special treatment. |
| if self.process_import_over_existing_name(name, existing_symbol, node, context): |
| return |
| |
| symbol_node: SymbolNode | None = node.node |
| |
| if self.is_class_scope(): |
| symbol_node = self._get_node_for_class_scoped_import(name, symbol_node, context) |
| |
| symbol = SymbolTableNode( |
| node.kind, symbol_node, module_public=module_public, module_hidden=module_hidden |
| ) |
| self.add_symbol_table_node(name, symbol, context) |
| |
| def add_unknown_imported_symbol( |
| self, |
| name: str, |
| context: Context, |
| target_name: str | None, |
| module_public: bool, |
| module_hidden: bool, |
| ) -> None: |
| """Add symbol that we don't know what it points to because resolving an import failed. |
| |
| This can happen if a module is missing, or it is present, but doesn't have |
| the imported attribute. The `target_name` is the name of symbol in the namespace |
| it is imported from. For example, for 'from mod import x as y' the target_name is |
| 'mod.x'. This is currently used only to track logical dependencies. |
| """ |
| existing = self.current_symbol_table().get(name) |
| if existing and isinstance(existing.node, Var) and existing.node.is_suppressed_import: |
| # This missing import was already added -- nothing to do here. |
| return |
| var = Var(name) |
| if self.options.logical_deps and target_name is not None: |
| # This makes it possible to add logical fine-grained dependencies |
| # from a missing module. We can't use this by default, since in a |
| # few places we assume that the full name points to a real |
| # definition, but this name may point to nothing. |
| var._fullname = target_name |
| elif self.type: |
| var._fullname = self.type.fullname + "." + name |
| var.info = self.type |
| else: |
| var._fullname = self.qualified_name(name) |
| var.is_ready = True |
| any_type = AnyType(TypeOfAny.from_unimported_type, missing_import_name=var._fullname) |
| var.type = any_type |
| var.is_suppressed_import = True |
| self.add_symbol( |
| name, var, context, module_public=module_public, module_hidden=module_hidden |
| ) |
| |
| # |
| # Other helpers |
| # |
| |
| @contextmanager |
| def tvar_scope_frame(self, frame: TypeVarLikeScope) -> Iterator[None]: |
| old_scope = self.tvar_scope |
| self.tvar_scope = frame |
| yield |
| self.tvar_scope = old_scope |
| |
| def defer(self, debug_context: Context | None = None, force_progress: bool = False) -> None: |
| """Defer current analysis target to be analyzed again. |
| |
| This must be called if something in the current target is |
| incomplete or has a placeholder node. However, this must *not* |
| be called during the final analysis iteration! Instead, an error |
| should be generated. Often 'process_placeholder' is a good |
| way to either defer or generate an error. |
| |
| NOTE: Some methods, such as 'anal_type', 'mark_incomplete' and |
| 'record_incomplete_ref', call this implicitly, or when needed. |
| They are usually preferable to a direct defer() call. |
| """ |
| assert not self.final_iteration, "Must not defer during final iteration" |
| if force_progress: |
| # Usually, we report progress if we have replaced a placeholder node |
| # with an actual valid node. However, sometimes we need to update an |
| # existing node *in-place*. For example, this is used by type aliases |
| # in context of forward references and/or recursive aliases, and in |
| # similar situations (recursive named tuples etc). |
| self.progress = True |
| self.deferred = True |
| # Store debug info for this deferral. |
| line = ( |
| debug_context.line if debug_context else self.statement.line if self.statement else -1 |
| ) |
| self.deferral_debug_context.append((self.cur_mod_id, line)) |
| |
| def track_incomplete_refs(self) -> Tag: |
| """Return tag that can be used for tracking references to incomplete names.""" |
| return self.num_incomplete_refs |
| |
| def found_incomplete_ref(self, tag: Tag) -> bool: |
| """Have we encountered an incomplete reference since starting tracking?""" |
| return self.num_incomplete_refs != tag |
| |
| def record_incomplete_ref(self) -> None: |
| """Record the encounter of an incomplete reference and defer current analysis target.""" |
| self.defer() |
| self.num_incomplete_refs += 1 |
| |
| def mark_incomplete( |
| self, |
| name: str, |
| node: Node, |
| becomes_typeinfo: bool = False, |
| module_public: bool = True, |
| module_hidden: bool = False, |
| ) -> None: |
| """Mark a definition as incomplete (and defer current analysis target). |
| |
| Also potentially mark the current namespace as incomplete. |
| |
| Args: |
| name: The name that we weren't able to define (or '*' if the name is unknown) |
| node: The node that refers to the name (definition or lvalue) |
| becomes_typeinfo: Pass this to PlaceholderNode (used by special forms like |
| named tuples that will create TypeInfos). |
| """ |
| self.defer(node) |
| if name == "*": |
| self.incomplete = True |
| elif not self.is_global_or_nonlocal(name): |
| fullname = self.qualified_name(name) |
| assert self.statement |
| placeholder = PlaceholderNode( |
| fullname, node, self.statement.line, becomes_typeinfo=becomes_typeinfo |
| ) |
| self.add_symbol( |
| name, |
| placeholder, |
| module_public=module_public, |
| module_hidden=module_hidden, |
| context=dummy_context(), |
| ) |
| self.missing_names[-1].add(name) |
| |
| def is_incomplete_namespace(self, fullname: str) -> bool: |
| """Is a module or class namespace potentially missing some definitions? |
| |
| If a name is missing from an incomplete namespace, we'll need to defer the |
| current analysis target. |
| """ |
| return fullname in self.incomplete_namespaces |
| |
| def process_placeholder( |
| self, name: str | None, kind: str, ctx: Context, force_progress: bool = False |
| ) -> None: |
| """Process a reference targeting placeholder node. |
| |
| If this is not a final iteration, defer current node, |
| otherwise report an error. |
| |
| The 'kind' argument indicates if this a name or attribute expression |
| (used for better error message). |
| """ |
| if self.final_iteration: |
| self.cannot_resolve_name(name, kind, ctx) |
| else: |
| self.defer(ctx, force_progress=force_progress) |
| |
| def cannot_resolve_name(self, name: str | None, kind: str, ctx: Context) -> None: |
| name_format = f' "{name}"' if name else "" |
| self.fail(f"Cannot resolve {kind}{name_format} (possible cyclic definition)", ctx) |
| if not self.options.disable_recursive_aliases and self.is_func_scope(): |
| self.note("Recursive types are not allowed at function scope", ctx) |
| |
| def qualified_name(self, name: str) -> str: |
| if self.type is not None: |
| return self.type._fullname + "." + name |
| elif self.is_func_scope(): |
| return name |
| else: |
| return self.cur_mod_id + "." + name |
| |
| @contextmanager |
| def enter( |
| self, function: FuncItem | GeneratorExpr | DictionaryComprehension |
| ) -> Iterator[None]: |
| """Enter a function, generator or comprehension scope.""" |
| names = self.saved_locals.setdefault(function, SymbolTable()) |
| self.locals.append(names) |
| is_comprehension = isinstance(function, (GeneratorExpr, DictionaryComprehension)) |
| self.is_comprehension_stack.append(is_comprehension) |
| self.global_decls.append(set()) |
| self.nonlocal_decls.append(set()) |
| # -1 since entering block will increment this to 0. |
| self.block_depth.append(-1) |
| self.loop_depth.append(0) |
| self.missing_names.append(set()) |
| try: |
| yield |
| finally: |
| self.locals.pop() |
| self.is_comprehension_stack.pop() |
| self.global_decls.pop() |
| self.nonlocal_decls.pop() |
| self.block_depth.pop() |
| self.loop_depth.pop() |
| self.missing_names.pop() |
| |
| def is_func_scope(self) -> bool: |
| return self.locals[-1] is not None |
| |
| def is_nested_within_func_scope(self) -> bool: |
| """Are we underneath a function scope, even if we are in a nested class also?""" |
| return any(l is not None for l in self.locals) |
| |
| def is_class_scope(self) -> bool: |
| return self.type is not None and not self.is_func_scope() |
| |
| def is_module_scope(self) -> bool: |
| return not (self.is_class_scope() or self.is_func_scope()) |
| |
| def current_symbol_kind(self) -> int: |
| if self.is_class_scope(): |
| kind = MDEF |
| elif self.is_func_scope(): |
| kind = LDEF |
| else: |
| kind = GDEF |
| return kind |
| |
| def current_symbol_table(self, escape_comprehensions: bool = False) -> SymbolTable: |
| if self.is_func_scope(): |
| assert self.locals[-1] is not None |
| if escape_comprehensions: |
| assert len(self.locals) == len(self.is_comprehension_stack) |
| # Retrieve the symbol table from the enclosing non-comprehension scope. |
| for i, is_comprehension in enumerate(reversed(self.is_comprehension_stack)): |
| if not is_comprehension: |
| if i == len(self.locals) - 1: # The last iteration. |
| # The caller of the comprehension is in the global space. |
| names = self.globals |
| else: |
| names_candidate = self.locals[-1 - i] |
| assert ( |
| names_candidate is not None |
| ), "Escaping comprehension from invalid scope" |
| names = names_candidate |
| break |
| else: |
| assert False, "Should have at least one non-comprehension scope" |
| else: |
| names = self.locals[-1] |
| assert names is not None |
| elif self.type is not None: |
| names = self.type.names |
| else: |
| names = self.globals |
| return names |
| |
| def is_global_or_nonlocal(self, name: str) -> bool: |
| return self.is_func_scope() and ( |
| name in self.global_decls[-1] or name in self.nonlocal_decls[-1] |
| ) |
| |
| def add_exports(self, exp_or_exps: Iterable[Expression] | Expression) -> None: |
| exps = [exp_or_exps] if isinstance(exp_or_exps, Expression) else exp_or_exps |
| for exp in exps: |
| if isinstance(exp, StrExpr): |
| self.all_exports.append(exp.value) |
| |
| def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None) -> None: |
| incomplete = self.is_incomplete_namespace(namespace or self.cur_mod_id) |
| if ( |
| namespace is None |
| and self.type |
| and not self.is_func_scope() |
| and self.incomplete_type_stack[-1] |
| and not self.final_iteration |
| ): |
| # We are processing a class body for the first time, so it is incomplete. |
| incomplete = True |
| if incomplete: |
| # Target namespace is incomplete, so it's possible that the name will be defined |
| # later on. Defer current target. |
| self.record_incomplete_ref() |
| return |
| message = f'Name "{name}" is not defined' |
| self.fail(message, ctx, code=codes.NAME_DEFINED) |
| |
| if f"builtins.{name}" in SUGGESTED_TEST_FIXTURES: |
| # The user probably has a missing definition in a test fixture. Let's verify. |
| fullname = f"builtins.{name}" |
| if self.lookup_fully_qualified_or_none(fullname) is None: |
| # Yes. Generate a helpful note. |
| self.msg.add_fixture_note(fullname, ctx) |
| |
| modules_with_unimported_hints = { |
| name.split(".", 1)[0] for name in TYPES_FOR_UNIMPORTED_HINTS |
| } |
| lowercased = {name.lower(): name for name in TYPES_FOR_UNIMPORTED_HINTS} |
| for module in modules_with_unimported_hints: |
| fullname = f"{module}.{name}".lower() |
| if fullname not in lowercased: |
| continue |
| # User probably forgot to import these types. |
| hint = ( |
| 'Did you forget to import it from "{module}"?' |
| ' (Suggestion: "from {module} import {name}")' |
| ).format(module=module, name=lowercased[fullname].rsplit(".", 1)[-1]) |
| self.note(hint, ctx, code=codes.NAME_DEFINED) |
| |
| def already_defined( |
| self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None, noun: str |
| ) -> None: |
| if isinstance(original_ctx, SymbolTableNode): |
| node: SymbolNode | None = original_ctx.node |
| elif isinstance(original_ctx, SymbolNode): |
| node = original_ctx |
| else: |
| node = None |
| |
| if isinstance(original_ctx, SymbolTableNode) and isinstance(original_ctx.node, MypyFile): |
| # Since this is an import, original_ctx.node points to the module definition. |
| # Therefore its line number is always 1, which is not useful for this |
| # error message. |
| extra_msg = " (by an import)" |
| elif node and node.line != -1 and self.is_local_name(node.fullname): |
| # TODO: Using previous symbol node may give wrong line. We should use |
| # the line number where the binding was established instead. |
| extra_msg = f" on line {node.line}" |
| else: |
| extra_msg = " (possibly by an import)" |
| self.fail( |
| f'{noun} "{unmangle(name)}" already defined{extra_msg}', ctx, code=codes.NO_REDEF |
| ) |
| |
| def name_already_defined( |
| self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None = None |
| ) -> None: |
| self.already_defined(name, ctx, original_ctx, noun="Name") |
| |
| def attribute_already_defined( |
| self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None = None |
| ) -> None: |
| self.already_defined(name, ctx, original_ctx, noun="Attribute") |
| |
| def is_local_name(self, name: str) -> bool: |
| """Does name look like reference to a definition in the current module?""" |
| return self.is_defined_in_current_module(name) or "." not in name |
| |
| def in_checked_function(self) -> bool: |
| """Should we type-check the current function? |
| |
| - Yes if --check-untyped-defs is set. |
| - Yes outside functions. |
| - Yes in annotated functions. |
| - No otherwise. |
| """ |
| if self.options.check_untyped_defs or not self.function_stack: |
| return True |
| |
| current_index = len(self.function_stack) - 1 |
| while current_index >= 0: |
| current_func = self.function_stack[current_index] |
| if not isinstance(current_func, LambdaExpr): |
| return not current_func.is_dynamic() |
| |
| # Special case, `lambda` inherits the "checked" state from its parent. |
| # Because `lambda` itself cannot be annotated. |
| # `lambdas` can be deeply nested, so we try to find at least one other parent. |
| current_index -= 1 |
| |
| # This means that we only have a stack of `lambda` functions, |
| # no regular functions. |
| return True |
| |
| def fail( |
| self, |
| msg: str, |
| ctx: Context, |
| serious: bool = False, |
| *, |
| code: ErrorCode | None = None, |
| blocker: bool = False, |
| ) -> None: |
| if not serious and not self.in_checked_function(): |
| return |
| # In case it's a bug and we don't really have context |
| assert ctx is not None, msg |
| self.errors.report(ctx.line, ctx.column, msg, blocker=blocker, code=code) |
| |
| def note(self, msg: str, ctx: Context, code: ErrorCode | None = None) -> None: |
| if not self.in_checked_function(): |
| return |
| self.errors.report(ctx.line, ctx.column, msg, severity="note", code=code) |
| |
| def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool: |
| if feature not in self.options.enable_incomplete_feature: |
| self.fail( |
| f'"{feature}" support is experimental,' |
| f" use --enable-incomplete-feature={feature} to enable", |
| ctx, |
| ) |
| return False |
| return True |
| |
| def accept(self, node: Node) -> None: |
| try: |
| node.accept(self) |
| except Exception as err: |
| report_internal_error(err, self.errors.file, node.line, self.errors, self.options) |
| |
| def expr_to_analyzed_type( |
| self, |
| expr: Expression, |
| report_invalid_types: bool = True, |
| allow_placeholder: bool = False, |
| allow_type_any: bool = False, |
| allow_unbound_tvars: bool = False, |
| allow_param_spec_literals: bool = False, |
| ) -> Type | None: |
| if isinstance(expr, CallExpr): |
| # This is a legacy syntax intended mostly for Python 2, we keep it for |
| # backwards compatibility, but new features like generic named tuples |
| # and recursive named tuples will be not supported. |
| expr.accept(self) |
| internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple( |
| expr, None, self.is_func_scope() |
| ) |
| if tvar_defs: |
| self.fail("Generic named tuples are not supported for legacy class syntax", expr) |
| self.note("Use either Python 3 class syntax, or the assignment syntax", expr) |
| if internal_name is None: |
| # Some form of namedtuple is the only valid type that looks like a call |
| # expression. This isn't a valid type. |
| raise TypeTranslationError() |
| elif not info: |
| self.defer(expr) |
| return None |
| assert info.tuple_type, "NamedTuple without tuple type" |
| fallback = Instance(info, []) |
| return TupleType(info.tuple_type.items, fallback=fallback) |
| typ = self.expr_to_unanalyzed_type(expr) |
| return self.anal_type( |
| typ, |
| report_invalid_types=report_invalid_types, |
| allow_placeholder=allow_placeholder, |
| allow_type_any=allow_type_any, |
| allow_unbound_tvars=allow_unbound_tvars, |
| allow_param_spec_literals=allow_param_spec_literals, |
| ) |
| |
| def analyze_type_expr(self, expr: Expression) -> None: |
| # There are certain expressions that mypy does not need to semantically analyze, |
| # since they analyzed solely as type. (For example, indexes in type alias definitions |
| # and base classes in class defs). External consumers of the mypy AST may need |
| # them semantically analyzed, however, if they need to treat it as an expression |
| # and not a type. (Which is to say, mypyc needs to do this.) Do the analysis |
| # in a fresh tvar scope in order to suppress any errors about using type variables. |
| with self.tvar_scope_frame(TypeVarLikeScope()), self.allow_unbound_tvars_set(): |
| expr.accept(self) |
| |
| def type_analyzer( |
| self, |
| *, |
| tvar_scope: TypeVarLikeScope | None = None, |
| allow_tuple_literal: bool = False, |
| allow_unbound_tvars: bool = False, |
| allow_placeholder: bool = False, |
| allow_required: bool = False, |
| allow_param_spec_literals: bool = False, |
| report_invalid_types: bool = True, |
| prohibit_self_type: str | None = None, |
| allow_type_any: bool = False, |
| ) -> TypeAnalyser: |
| if tvar_scope is None: |
| tvar_scope = self.tvar_scope |
| tpan = TypeAnalyser( |
| self, |
| tvar_scope, |
| self.plugin, |
| self.options, |
| self.is_typeshed_stub_file, |
| allow_unbound_tvars=allow_unbound_tvars, |
| allow_tuple_literal=allow_tuple_literal, |
| report_invalid_types=report_invalid_types, |
| allow_placeholder=allow_placeholder, |
| allow_required=allow_required, |
| allow_param_spec_literals=allow_param_spec_literals, |
| prohibit_self_type=prohibit_self_type, |
| allow_type_any=allow_type_any, |
| ) |
| tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic()) |
| tpan.global_scope = not self.type and not self.function_stack |
| return tpan |
| |
| def expr_to_unanalyzed_type(self, node: Expression) -> ProperType: |
| return expr_to_unanalyzed_type(node, self.options, self.is_stub_file) |
| |
| def anal_type( |
| self, |
| typ: Type, |
| *, |
| tvar_scope: TypeVarLikeScope | None = None, |
| allow_tuple_literal: bool = False, |
| allow_unbound_tvars: bool = False, |
| allow_placeholder: bool = False, |
| allow_required: bool = False, |
| allow_param_spec_literals: bool = False, |
| report_invalid_types: bool = True, |
| prohibit_self_type: str | None = None, |
| allow_type_any: bool = False, |
| third_pass: bool = False, |
| ) -> Type | None: |
| """Semantically analyze a type. |
| |
| Args: |
| typ: Type to analyze (if already analyzed, this is a no-op) |
| allow_placeholder: If True, may return PlaceholderType if |
| encountering an incomplete definition |
| third_pass: Unused; only for compatibility with old semantic |
| analyzer |
| |
| Return None only if some part of the type couldn't be bound *and* it |
| referred to an incomplete namespace or definition. In this case also |
| defer as needed. During a final iteration this won't return None; |
| instead report an error if the type can't be analyzed and return |
| AnyType. |
| |
| In case of other errors, report an error message and return AnyType. |
| |
| NOTE: The caller shouldn't defer even if this returns None or a |
| placeholder type. |
| """ |
| has_self_type = find_self_type( |
| typ, lambda name: self.lookup_qualified(name, typ, suppress_errors=True) |
| ) |
| if has_self_type and self.type and prohibit_self_type is None: |
| self.setup_self_type() |
| a = self.type_analyzer( |
| tvar_scope=tvar_scope, |
| allow_unbound_tvars=allow_unbound_tvars, |
| allow_tuple_literal=allow_tuple_literal, |
| allow_placeholder=allow_placeholder, |
| allow_required=allow_required, |
| allow_param_spec_literals=allow_param_spec_literals, |
| report_invalid_types=report_invalid_types, |
| prohibit_self_type=prohibit_self_type, |
| allow_type_any=allow_type_any, |
| ) |
| tag = self.track_incomplete_refs() |
| typ = typ.accept(a) |
| if self.found_incomplete_ref(tag): |
| # Something could not be bound yet. |
| return None |
| self.add_type_alias_deps(a.aliases_used) |
| return typ |
| |
| def class_type(self, self_type: Type) -> Type: |
| return TypeType.make_normalized(self_type) |
| |
| def schedule_patch(self, priority: int, patch: Callable[[], None]) -> None: |
| self.patches.append((priority, patch)) |
| |
| def report_hang(self) -> None: |
| print("Deferral trace:") |
| for mod, line in self.deferral_debug_context: |
| print(f" {mod}:{line}") |
| self.errors.report( |
| -1, |
| -1, |
| "INTERNAL ERROR: maximum semantic analysis iteration count reached", |
| blocker=True, |
| ) |
| |
| def add_plugin_dependency(self, trigger: str, target: str | None = None) -> None: |
| """Add dependency from trigger to a target. |
| |
| If the target is not given explicitly, use the current target. |
| """ |
| if target is None: |
| target = self.scope.current_target() |
| self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target) |
| |
| def add_type_alias_deps( |
| self, aliases_used: Collection[str], target: str | None = None |
| ) -> None: |
| """Add full names of type aliases on which the current node depends. |
| |
| This is used by fine-grained incremental mode to re-check the corresponding nodes. |
| If `target` is None, then the target node used will be the current scope. |
| """ |
| if not aliases_used: |
| # A basic optimization to avoid adding targets with no dependencies to |
| # the `alias_deps` dict. |
| return |
| if target is None: |
| target = self.scope.current_target() |
| self.cur_mod_node.alias_deps[target].update(aliases_used) |
| |
| def is_mangled_global(self, name: str) -> bool: |
| # A global is mangled if there exists at least one renamed variant. |
| return unmangle(name) + "'" in self.globals |
| |
| def is_initial_mangled_global(self, name: str) -> bool: |
| # If there are renamed definitions for a global, the first one has exactly one prime. |
| return name == unmangle(name) + "'" |
| |
| def parse_bool(self, expr: Expression) -> bool | None: |
| # This wrapper is preserved for plugins. |
| return parse_bool(expr) |
| |
| def parse_str_literal(self, expr: Expression) -> str | None: |
| """Attempt to find the string literal value of the given expression. Returns `None` if no |
| literal value can be found.""" |
| if isinstance(expr, StrExpr): |
| return expr.value |
| if isinstance(expr, RefExpr) and isinstance(expr.node, Var) and expr.node.type is not None: |
| values = try_getting_str_literals_from_type(expr.node.type) |
| if values is not None and len(values) == 1: |
| return values[0] |
| return None |
| |
| def set_future_import_flags(self, module_name: str) -> None: |
| if module_name in FUTURE_IMPORTS: |
| self.modules[self.cur_mod_id].future_import_flags.add(FUTURE_IMPORTS[module_name]) |
| |
| def is_future_flag_set(self, flag: str) -> bool: |
| return self.modules[self.cur_mod_id].is_future_flag_set(flag) |
| |
| def parse_dataclass_transform_spec(self, call: CallExpr) -> DataclassTransformSpec: |
| """Build a DataclassTransformSpec from the arguments passed to the given call to |
| typing.dataclass_transform.""" |
| parameters = DataclassTransformSpec() |
| for name, value in zip(call.arg_names, call.args): |
| # Skip any positional args. Note that any such args are invalid, but we can rely on |
| # typeshed to enforce this and don't need an additional error here. |
| if name is None: |
| continue |
| |
| # field_specifiers is currently the only non-boolean argument; check for it first so |
| # so the rest of the block can fail through to handling booleans |
| if name == "field_specifiers": |
| parameters.field_specifiers = self.parse_dataclass_transform_field_specifiers( |
| value |
| ) |
| continue |
| |
| boolean = require_bool_literal_argument(self, value, name) |
| if boolean is None: |
| continue |
| |
| if name == "eq_default": |
| parameters.eq_default = boolean |
| elif name == "order_default": |
| parameters.order_default = boolean |
| elif name == "kw_only_default": |
| parameters.kw_only_default = boolean |
| elif name == "frozen_default": |
| parameters.frozen_default = boolean |
| else: |
| self.fail(f'Unrecognized dataclass_transform parameter "{name}"', call) |
| |
| return parameters |
| |
| def parse_dataclass_transform_field_specifiers(self, arg: Expression) -> tuple[str, ...]: |
| if not isinstance(arg, TupleExpr): |
| self.fail('"field_specifiers" argument must be a tuple literal', arg) |
| return tuple() |
| |
| names = [] |
| for specifier in arg.items: |
| if not isinstance(specifier, RefExpr): |
| self.fail('"field_specifiers" must only contain identifiers', specifier) |
| return tuple() |
| names.append(specifier.fullname) |
| return tuple(names) |
| |
| |
| def replace_implicit_first_type(sig: FunctionLike, new: Type) -> FunctionLike: |
| if isinstance(sig, CallableType): |
| if len(sig.arg_types) == 0: |
| return sig |
| return sig.copy_modified(arg_types=[new] + sig.arg_types[1:]) |
| elif isinstance(sig, Overloaded): |
| return Overloaded( |
| [cast(CallableType, replace_implicit_first_type(i, new)) for i in sig.items] |
| ) |
| else: |
| assert False |
| |
| |
| def refers_to_fullname(node: Expression, fullnames: str | tuple[str, ...]) -> bool: |
| """Is node a name or member expression with the given full name?""" |
| if not isinstance(fullnames, tuple): |
| fullnames = (fullnames,) |
| |
| if not isinstance(node, RefExpr): |
| return False |
| if node.fullname in fullnames: |
| return True |
| if isinstance(node.node, TypeAlias): |
| return is_named_instance(node.node.target, fullnames) |
| return False |
| |
| |
| def refers_to_class_or_function(node: Expression) -> bool: |
| """Does semantically analyzed node refer to a class?""" |
| return isinstance(node, RefExpr) and isinstance( |
| node.node, (TypeInfo, FuncDef, OverloadedFuncDef) |
| ) |
| |
| |
| def find_duplicate(list: list[T]) -> T | None: |
| """If the list has duplicates, return one of the duplicates. |
| |
| Otherwise, return None. |
| """ |
| for i in range(1, len(list)): |
| if list[i] in list[:i]: |
| return list[i] |
| return None |
| |
| |
| def remove_imported_names_from_symtable(names: SymbolTable, module: str) -> None: |
| """Remove all imported names from the symbol table of a module.""" |
| removed: list[str] = [] |
| for name, node in names.items(): |
| if node.node is None: |
| continue |
| fullname = node.node.fullname |
| prefix = fullname[: fullname.rfind(".")] |
| if prefix != module: |
| removed.append(name) |
| for name in removed: |
| del names[name] |
| |
| |
| def make_any_non_explicit(t: Type) -> Type: |
| """Replace all Any types within in with Any that has attribute 'explicit' set to False""" |
| return t.accept(MakeAnyNonExplicit()) |
| |
| |
| class MakeAnyNonExplicit(TrivialSyntheticTypeTranslator): |
| def visit_any(self, t: AnyType) -> Type: |
| if t.type_of_any == TypeOfAny.explicit: |
| return t.copy_modified(TypeOfAny.special_form) |
| return t |
| |
| def visit_type_alias_type(self, t: TypeAliasType) -> Type: |
| return t.copy_modified(args=[a.accept(self) for a in t.args]) |
| |
| |
| def apply_semantic_analyzer_patches(patches: list[tuple[int, Callable[[], None]]]) -> None: |
| """Call patch callbacks in the right order. |
| |
| This should happen after semantic analyzer pass 3. |
| """ |
| patches_by_priority = sorted(patches, key=lambda x: x[0]) |
| for priority, patch_func in patches_by_priority: |
| patch_func() |
| |
| |
| def names_modified_by_assignment(s: AssignmentStmt) -> list[NameExpr]: |
| """Return all unqualified (short) names assigned to in an assignment statement.""" |
| result: list[NameExpr] = [] |
| for lvalue in s.lvalues: |
| result += names_modified_in_lvalue(lvalue) |
| return result |
| |
| |
| def names_modified_in_lvalue(lvalue: Lvalue) -> list[NameExpr]: |
| """Return all NameExpr assignment targets in an Lvalue.""" |
| if isinstance(lvalue, NameExpr): |
| return [lvalue] |
| elif isinstance(lvalue, StarExpr): |
| return names_modified_in_lvalue(lvalue.expr) |
| elif isinstance(lvalue, (ListExpr, TupleExpr)): |
| result: list[NameExpr] = [] |
| for item in lvalue.items: |
| result += names_modified_in_lvalue(item) |
| return result |
| return [] |
| |
| |
| def is_same_var_from_getattr(n1: SymbolNode | None, n2: SymbolNode | None) -> bool: |
| """Do n1 and n2 refer to the same Var derived from module-level __getattr__?""" |
| return ( |
| isinstance(n1, Var) |
| and n1.from_module_getattr |
| and isinstance(n2, Var) |
| and n2.from_module_getattr |
| and n1.fullname == n2.fullname |
| ) |
| |
| |
| def dummy_context() -> Context: |
| return TempNode(AnyType(TypeOfAny.special_form)) |
| |
| |
| def is_valid_replacement(old: SymbolTableNode, new: SymbolTableNode) -> bool: |
| """Can symbol table node replace an existing one? |
| |
| These are the only valid cases: |
| |
| 1. Placeholder gets replaced with a non-placeholder |
| 2. Placeholder that isn't known to become type replaced with a |
| placeholder that can become a type |
| """ |
| if isinstance(old.node, PlaceholderNode): |
| if isinstance(new.node, PlaceholderNode): |
| return not old.node.becomes_typeinfo and new.node.becomes_typeinfo |
| else: |
| return True |
| return False |
| |
| |
| def is_same_symbol(a: SymbolNode | None, b: SymbolNode | None) -> bool: |
| return ( |
| a == b |
| or (isinstance(a, PlaceholderNode) and isinstance(b, PlaceholderNode)) |
| or is_same_var_from_getattr(a, b) |
| ) |
| |
| |
| def is_trivial_body(block: Block) -> bool: |
| """Returns 'true' if the given body is "trivial" -- if it contains just a "pass", |
| "..." (ellipsis), or "raise NotImplementedError()". A trivial body may also |
| start with a statement containing just a string (e.g. a docstring). |
| |
| Note: Functions that raise other kinds of exceptions do not count as |
| "trivial". We use this function to help us determine when it's ok to |
| relax certain checks on body, but functions that raise arbitrary exceptions |
| are more likely to do non-trivial work. For example: |
| |
| def halt(self, reason: str = ...) -> NoReturn: |
| raise MyCustomError("Fatal error: " + reason, self.line, self.context) |
| |
| A function that raises just NotImplementedError is much less likely to be |
| this complex. |
| |
| Note: If you update this, you may also need to update |
| mypy.fastparse.is_possible_trivial_body! |
| """ |
| body = block.body |
| if not body: |
| # Functions have empty bodies only if the body is stripped or the function is |
| # generated or deserialized. In these cases the body is unknown. |
| return False |
| |
| # Skip a docstring |
| if isinstance(body[0], ExpressionStmt) and isinstance(body[0].expr, StrExpr): |
| body = block.body[1:] |
| |
| if len(body) == 0: |
| # There's only a docstring (or no body at all). |
| return True |
| elif len(body) > 1: |
| return False |
| |
| stmt = body[0] |
| |
| if isinstance(stmt, RaiseStmt): |
| expr = stmt.expr |
| if expr is None: |
| return False |
| if isinstance(expr, CallExpr): |
| expr = expr.callee |
| |
| return isinstance(expr, NameExpr) and expr.fullname == "builtins.NotImplementedError" |
| |
| return isinstance(stmt, PassStmt) or ( |
| isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr) |
| ) |