From a90e404c3f010446ab8c18b4793c78834eeb65b7 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 22 Nov 2024 08:47:14 +0100 Subject: [PATCH] [red-knot] PEP 695 type aliases (#14357) ## Summary Add support for (non-generic) type aliases. The main motivation behind this was to get rid of panics involving expressions in (generic) type aliases. But it turned out the best way to fix it was to implement (partial) support for type aliases. ```py type IntOrStr = int | str reveal_type(IntOrStr) # revealed: typing.TypeAliasType reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"] x: IntOrStr = 1 reveal_type(x) # revealed: Literal[1] def f() -> None: reveal_type(x) # revealed: int | str ``` ## Test Plan - Updated corpus test allow list to reflect that we don't panic anymore. - Added Markdown-based test for type aliases (`type_alias.md`) --- .../resources/mdtest/type_alias.md | 71 +++++++++++++++++ .../src/semantic_index/builder.rs | 24 +++++- .../src/semantic_index/definition.rs | 19 +++++ .../src/semantic_index/symbol.rs | 53 ++++++++++--- crates/red_knot_python_semantic/src/types.rs | 79 +++++++++++++++++-- .../src/types/infer.rs | 75 ++++++++++++++---- .../red_knot_python_semantic/src/types/mro.rs | 1 + .../corpus/89_type_alias_invalid_bound.py | 1 + crates/red_knot_workspace/tests/check.rs | 26 ++---- 9 files changed, 298 insertions(+), 51 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/type_alias.md create mode 120000 crates/red_knot_workspace/resources/test/corpus/89_type_alias_invalid_bound.py diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_alias.md b/crates/red_knot_python_semantic/resources/mdtest/type_alias.md new file mode 100644 index 0000000000..77a9441136 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/type_alias.md @@ -0,0 +1,71 @@ +# Type aliases + +## Basic + +```py +type IntOrStr = int | str + +reveal_type(IntOrStr) # revealed: typing.TypeAliasType +reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"] + +x: IntOrStr = 1 + +reveal_type(x) # revealed: Literal[1] + +def f() -> None: + reveal_type(x) # revealed: int | str +``` + +## `__value__` attribute + +```py +type IntOrStr = int | str + +# TODO: This should either fall back to the specified type from typeshed, +# which is `Any`, or be the actual type of the runtime value expression +# `int | str`, i.e. `types.UnionType`. +reveal_type(IntOrStr.__value__) # revealed: @Todo(instance attributes) +``` + +## Invalid assignment + +```py +type OptionalInt = int | None + +# error: [invalid-assignment] +x: OptionalInt = "1" +``` + +## Type aliases in type aliases + +```py +type IntOrStr = int | str +type IntOrStrOrBytes = IntOrStr | bytes + +x: IntOrStrOrBytes = 1 + +def f() -> None: + reveal_type(x) # revealed: int | str | bytes +``` + +## Aliased type aliases + +```py +type IntOrStr = int | str +MyIntOrStr = IntOrStr + +x: MyIntOrStr = 1 + +# error: [invalid-assignment] +y: MyIntOrStr = None +``` + +## Generic type aliases + +```py +type ListOrSet[T] = list[T] | set[T] + +# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`, +# as specified in the `typeshed` stubs. +reveal_type(ListOrSet.__type_params__) # revealed: @Todo(instance attributes) +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 88f3d99a56..a1c7a5e0ed 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -131,7 +131,8 @@ impl<'db> SemanticIndexBuilder<'db> { let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default()); self.scope_ids_by_scope.push(scope_id); - self.scopes_by_node.insert(node.node_key(), file_scope_id); + let previous = self.scopes_by_node.insert(node.node_key(), file_scope_id); + debug_assert_eq!(previous, None); debug_assert_eq!(ast_id_scope, file_scope_id); @@ -589,6 +590,27 @@ where }, ); } + ast::Stmt::TypeAlias(type_alias) => { + let symbol = self.add_symbol( + type_alias + .name + .as_name_expr() + .map(|name| name.id.clone()) + .unwrap_or("".into()), + ); + self.add_definition(symbol, type_alias); + self.visit_expr(&type_alias.name); + + self.with_type_params( + NodeWithScopeRef::TypeAliasTypeParameters(type_alias), + type_alias.type_params.as_ref(), + |builder| { + builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias)); + builder.visit_expr(&type_alias.value); + builder.pop_scope() + }, + ); + } ast::Stmt::Import(node) => { for alias in &node.names { let symbol_name = if let Some(asname) = &alias.asname { diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs index 11822e5d75..82f8cf7557 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/definition.rs @@ -83,6 +83,7 @@ pub(crate) enum DefinitionNodeRef<'a> { For(ForStmtDefinitionNodeRef<'a>), Function(&'a ast::StmtFunctionDef), Class(&'a ast::StmtClassDef), + TypeAlias(&'a ast::StmtTypeAlias), NamedExpression(&'a ast::ExprNamed), Assignment(AssignmentDefinitionNodeRef<'a>), AnnotatedAssignment(&'a ast::StmtAnnAssign), @@ -109,6 +110,12 @@ impl<'a> From<&'a ast::StmtClassDef> for DefinitionNodeRef<'a> { } } +impl<'a> From<&'a ast::StmtTypeAlias> for DefinitionNodeRef<'a> { + fn from(node: &'a ast::StmtTypeAlias) -> Self { + Self::TypeAlias(node) + } +} + impl<'a> From<&'a ast::ExprNamed> for DefinitionNodeRef<'a> { fn from(node: &'a ast::ExprNamed) -> Self { Self::NamedExpression(node) @@ -265,6 +272,9 @@ impl<'db> DefinitionNodeRef<'db> { DefinitionNodeRef::Class(class) => { DefinitionKind::Class(AstNodeRef::new(parsed, class)) } + DefinitionNodeRef::TypeAlias(type_alias) => { + DefinitionKind::TypeAlias(AstNodeRef::new(parsed, type_alias)) + } DefinitionNodeRef::NamedExpression(named) => { DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named)) } @@ -358,6 +368,7 @@ impl<'db> DefinitionNodeRef<'db> { } Self::Function(node) => node.into(), Self::Class(node) => node.into(), + Self::TypeAlias(node) => node.into(), Self::NamedExpression(node) => node.into(), Self::Assignment(AssignmentDefinitionNodeRef { value: _, @@ -434,6 +445,7 @@ pub enum DefinitionKind<'db> { ImportFrom(ImportFromDefinitionKind), Function(AstNodeRef), Class(AstNodeRef), + TypeAlias(AstNodeRef), NamedExpression(AstNodeRef), Assignment(AssignmentDefinitionKind<'db>), AnnotatedAssignment(AstNodeRef), @@ -456,6 +468,7 @@ impl DefinitionKind<'_> { // functions, classes, and imports always bind, and we consider them declarations DefinitionKind::Function(_) | DefinitionKind::Class(_) + | DefinitionKind::TypeAlias(_) | DefinitionKind::Import(_) | DefinitionKind::ImportFrom(_) | DefinitionKind::TypeVar(_) @@ -682,6 +695,12 @@ impl From<&ast::StmtClassDef> for DefinitionNodeKey { } } +impl From<&ast::StmtTypeAlias> for DefinitionNodeKey { + fn from(node: &ast::StmtTypeAlias) -> Self { + Self(NodeKey::from_node(node)) + } +} + impl From<&ast::ExprName> for DefinitionNodeKey { fn from(node: &ast::ExprName) -> Self { Self(NodeKey::from_node(node)) diff --git a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs index 6866635d48..87dab330b8 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs @@ -116,14 +116,11 @@ impl<'db> ScopeId<'db> { // Type parameter scopes behave like function scopes in terms of name resolution; CPython // symbol table also uses the term "function-like" for these scopes. matches!( - self.node(db), - NodeWithScopeKind::ClassTypeParameters(_) - | NodeWithScopeKind::FunctionTypeParameters(_) - | NodeWithScopeKind::Function(_) - | NodeWithScopeKind::ListComprehension(_) - | NodeWithScopeKind::SetComprehension(_) - | NodeWithScopeKind::DictComprehension(_) - | NodeWithScopeKind::GeneratorExpression(_) + self.node(db).scope_kind(), + ScopeKind::Annotation + | ScopeKind::Function + | ScopeKind::TypeAlias + | ScopeKind::Comprehension ) } @@ -144,6 +141,12 @@ impl<'db> ScopeId<'db> { } NodeWithScopeKind::Function(function) | NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(), + NodeWithScopeKind::TypeAlias(type_alias) + | NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias + .name + .as_name_expr() + .map(|name| name.id.as_str()) + .unwrap_or(""), NodeWithScopeKind::Lambda(_) => "", NodeWithScopeKind::ListComprehension(_) => "", NodeWithScopeKind::SetComprehension(_) => "", @@ -201,6 +204,7 @@ pub enum ScopeKind { Class, Function, Comprehension, + TypeAlias, } impl ScopeKind { @@ -326,6 +330,8 @@ pub(crate) enum NodeWithScopeRef<'a> { Lambda(&'a ast::ExprLambda), FunctionTypeParameters(&'a ast::StmtFunctionDef), ClassTypeParameters(&'a ast::StmtClassDef), + TypeAlias(&'a ast::StmtTypeAlias), + TypeAliasTypeParameters(&'a ast::StmtTypeAlias), ListComprehension(&'a ast::ExprListComp), SetComprehension(&'a ast::ExprSetComp), DictComprehension(&'a ast::ExprDictComp), @@ -347,6 +353,12 @@ impl NodeWithScopeRef<'_> { NodeWithScopeRef::Function(function) => { NodeWithScopeKind::Function(AstNodeRef::new(module, function)) } + NodeWithScopeRef::TypeAlias(type_alias) => { + NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias)) + } + NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { + NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias)) + } NodeWithScopeRef::Lambda(lambda) => { NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda)) } @@ -387,6 +399,12 @@ impl NodeWithScopeRef<'_> { NodeWithScopeRef::ClassTypeParameters(class) => { NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class)) } + NodeWithScopeRef::TypeAlias(type_alias) => { + NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias)) + } + NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { + NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias)) + } NodeWithScopeRef::ListComprehension(comprehension) => { NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension)) } @@ -411,6 +429,8 @@ pub enum NodeWithScopeKind { ClassTypeParameters(AstNodeRef), Function(AstNodeRef), FunctionTypeParameters(AstNodeRef), + TypeAliasTypeParameters(AstNodeRef), + TypeAlias(AstNodeRef), Lambda(AstNodeRef), ListComprehension(AstNodeRef), SetComprehension(AstNodeRef), @@ -423,9 +443,11 @@ impl NodeWithScopeKind { match self { Self::Module => ScopeKind::Module, Self::Class(_) => ScopeKind::Class, - Self::Function(_) => ScopeKind::Function, - Self::Lambda(_) => ScopeKind::Function, - Self::FunctionTypeParameters(_) | Self::ClassTypeParameters(_) => ScopeKind::Annotation, + Self::Function(_) | Self::Lambda(_) => ScopeKind::Function, + Self::FunctionTypeParameters(_) + | Self::ClassTypeParameters(_) + | Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation, + Self::TypeAlias(_) => ScopeKind::TypeAlias, Self::ListComprehension(_) | Self::SetComprehension(_) | Self::DictComprehension(_) @@ -446,6 +468,13 @@ impl NodeWithScopeKind { _ => panic!("expected function"), } } + + pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias { + match self { + Self::TypeAlias(type_alias) => type_alias.node(), + _ => panic!("expected type alias"), + } + } } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -455,6 +484,8 @@ pub(crate) enum NodeWithScopeKey { ClassTypeParameters(NodeKey), Function(NodeKey), FunctionTypeParameters(NodeKey), + TypeAlias(NodeKey), + TypeAliasTypeParameters(NodeKey), Lambda(NodeKey), ListComprehension(NodeKey), SetComprehension(NodeKey), diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b19d52b4c9..89aa8077a8 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1065,7 +1065,10 @@ impl<'db> Type<'db> { Type::Instance(InstanceType { class }) => match class.known(db) { Some( - KnownClass::NoneType | KnownClass::NoDefaultType | KnownClass::VersionInfo, + KnownClass::NoneType + | KnownClass::NoDefaultType + | KnownClass::VersionInfo + | KnownClass::TypeAliasType, ) => true, Some( KnownClass::Bool @@ -1536,6 +1539,7 @@ impl<'db> Type<'db> { Type::Unknown => Type::Unknown, // TODO map this to a new `Type::TypeVar` variant Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self, + Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => alias.value_ty(db), _ => todo_type!(), } } @@ -1700,6 +1704,7 @@ pub enum KnownClass { // Typing SpecialForm, TypeVar, + TypeAliasType, NoDefaultType, // sys VersionInfo, @@ -1726,6 +1731,7 @@ impl<'db> KnownClass { Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", Self::TypeVar => "TypeVar", + Self::TypeAliasType => "TypeAliasType", Self::NoDefaultType => "_NoDefaultType", // This is the name the type of `sys.version_info` has in typeshed, // which is different to what `type(sys.version_info).__name__` is at runtime. @@ -1764,7 +1770,7 @@ impl<'db> KnownClass { Self::VersionInfo => CoreStdlibModule::Sys, Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types, Self::NoneType => CoreStdlibModule::Typeshed, - Self::SpecialForm | Self::TypeVar => CoreStdlibModule::Typing, + Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing, // TODO when we understand sys.version_info, we will need an explicit fallback here, // because typing_extensions has a 3.13+ re-export for the `typing.NoDefault` // singleton, but not for `typing._NoDefaultType` @@ -1778,7 +1784,7 @@ impl<'db> KnownClass { const fn is_singleton(self) -> bool { // TODO there are other singleton types (EllipsisType, NotImplementedType) match self { - Self::NoneType | Self::NoDefaultType | Self::VersionInfo => true, + Self::NoneType | Self::NoDefaultType | Self::VersionInfo | Self::TypeAliasType => true, Self::Bool | Self::Object | Self::Bytes @@ -1820,6 +1826,7 @@ impl<'db> KnownClass { "NoneType" => Self::NoneType, "ModuleType" => Self::ModuleType, "FunctionType" => Self::FunctionType, + "TypeAliasType" => Self::TypeAliasType, "_SpecialForm" => Self::SpecialForm, "_NoDefaultType" => Self::NoDefaultType, "_version_info" => Self::VersionInfo, @@ -1853,7 +1860,7 @@ impl<'db> KnownClass { | Self::VersionInfo | Self::FunctionType => module.name() == self.canonical_module().as_str(), Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"), - Self::SpecialForm | Self::TypeVar | Self::NoDefaultType => { + Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => { matches!(module.name().as_str(), "typing" | "typing_extensions") } } @@ -1871,6 +1878,8 @@ pub enum KnownInstanceType<'db> { Union, /// A single instance of `typing.TypeVar` TypeVar(TypeVarInstance<'db>), + /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) + TypeAliasType(TypeAliasType<'db>), // TODO: fill this enum out with more special forms, etc. } @@ -1881,15 +1890,18 @@ impl<'db> KnownInstanceType<'db> { KnownInstanceType::Optional => "Optional", KnownInstanceType::Union => "Union", KnownInstanceType::TypeVar(_) => "TypeVar", + KnownInstanceType::TypeAliasType(_) => "TypeAliasType", } } /// Evaluate the known instance in boolean context pub const fn bool(self) -> Truthiness { match self { - Self::Literal | Self::Optional | Self::TypeVar(_) | Self::Union => { - Truthiness::AlwaysTrue - } + Self::Literal + | Self::Optional + | Self::TypeVar(_) + | Self::Union + | Self::TypeAliasType(_) => Truthiness::AlwaysTrue, } } @@ -1900,6 +1912,7 @@ impl<'db> KnownInstanceType<'db> { Self::Optional => "typing.Optional", Self::Union => "typing.Union", Self::TypeVar(typevar) => typevar.name(db), + Self::TypeAliasType(_) => "typing.TypeAliasType", } } @@ -1910,6 +1923,7 @@ impl<'db> KnownInstanceType<'db> { Self::Optional => KnownClass::SpecialForm, Self::Union => KnownClass::SpecialForm, Self::TypeVar(_) => KnownClass::TypeVar, + Self::TypeAliasType(_) => KnownClass::TypeAliasType, } } @@ -1954,6 +1968,7 @@ impl<'db> KnownInstanceType<'db> { .default_ty(db) .map(|ty| ty.to_meta_type(db)) .unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db)), + (Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)), _ => return self.instance_fallback(db).member(db, name), }; ty.into() @@ -2779,6 +2794,27 @@ impl<'db> Class<'db> { } } +#[salsa::interned] +pub struct TypeAliasType<'db> { + #[return_ref] + pub name: ast::name::Name, + + rhs_scope: ScopeId<'db>, +} + +#[salsa::tracked] +impl<'db> TypeAliasType<'db> { + #[salsa::tracked] + pub fn value_ty(self, db: &'db dyn Db) -> Type<'db> { + let scope = self.rhs_scope(db); + + let type_alias_stmt_node = scope.node(db).expect_type_alias(); + let definition = semantic_index(db, scope.file(db)).definition(type_alias_stmt_node); + + definition_expression_ty(db, definition, &type_alias_stmt_node.value) + } +} + /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. #[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct MetaclassCandidate<'db> { @@ -3630,6 +3666,35 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn type_alias_types() -> anyhow::Result<()> { + let mut db = setup_db(); + + db.write_dedented( + "src/mod.py", + r#" + type Alias1 = int + type Alias2 = int + "#, + )?; + + let mod_py = system_path_to_file(&db, "src/mod.py")?; + let ty_alias1 = global_symbol(&db, mod_py, "Alias1").expect_type(); + let ty_alias2 = global_symbol(&db, mod_py, "Alias2").expect_type(); + + let Type::KnownInstance(KnownInstanceType::TypeAliasType(alias1)) = ty_alias1 else { + panic!("Expected TypeAliasType, got {ty_alias1:?}"); + }; + assert_eq!(alias1.name(&db), "Alias1"); + assert_eq!(alias1.value_ty(&db), KnownClass::Int.to_instance(&db)); + + // Two type aliases are distinct and disjoint, even if they refer to the same type + assert!(!ty_alias1.is_equivalent_to(&db, ty_alias2)); + assert!(ty_alias1.is_disjoint_from(&db, ty_alias2)); + + Ok(()) + } + /// All other tests also make sure that `Type::Todo` works as expected. This particular /// test makes sure that we handle `Todo` types correctly, even if they originate from /// different sources. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 42b4297dfd..853abbe261 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -56,8 +56,8 @@ use crate::types::{ typing_extensions_symbol, Boundness, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol, - Truthiness, TupleType, Type, TypeArrayDisplay, TypeVarBoundOrConstraints, TypeVarInstance, - UnionBuilder, UnionType, + Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, TypeVarBoundOrConstraints, + TypeVarInstance, UnionBuilder, UnionType, }; use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; @@ -439,6 +439,12 @@ impl<'db> TypeInferenceBuilder<'db> { NodeWithScopeKind::FunctionTypeParameters(function) => { self.infer_function_type_params(function.node()); } + NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => { + self.infer_type_alias_type_params(type_alias.node()); + } + NodeWithScopeKind::TypeAlias(type_alias) => { + self.infer_type_alias(type_alias.node()); + } NodeWithScopeKind::ListComprehension(comprehension) => { self.infer_list_comprehension_expression_scope(comprehension.node()); } @@ -606,6 +612,9 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_function_definition(function.node(), definition); } DefinitionKind::Class(class) => self.infer_class_definition(class.node(), definition), + DefinitionKind::TypeAlias(type_alias) => { + self.infer_type_alias_definition(type_alias.node(), definition); + } DefinitionKind::Import(import) => { self.infer_import_definition(import.node(), definition); } @@ -848,6 +857,19 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_parameters(&function.parameters); } + fn infer_type_alias_type_params(&mut self, type_alias: &ast::StmtTypeAlias) { + let type_params = type_alias + .type_params + .as_ref() + .expect("type alias type params scope without type params"); + + self.infer_type_parameters(type_params); + } + + fn infer_type_alias(&mut self, type_alias: &ast::StmtTypeAlias) { + self.infer_annotation_expression(&type_alias.value, DeferredExpressionState::Deferred); + } + fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) { self.infer_body(&function.body); } @@ -1108,6 +1130,33 @@ impl<'db> TypeInferenceBuilder<'db> { } } + fn infer_type_alias_definition( + &mut self, + type_alias: &ast::StmtTypeAlias, + definition: Definition<'db>, + ) { + self.infer_expression(&type_alias.name); + + let rhs_scope = self + .index + .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) + .to_scope_id(self.db, self.file); + + let type_alias_ty = + Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::new( + self.db, + &type_alias.name.as_name_expr().unwrap().id, + rhs_scope, + ))); + + self.add_declaration_with_binding( + type_alias.into(), + definition, + type_alias_ty, + type_alias_ty, + ); + } + fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) { let ast::StmtIf { range: _, @@ -1830,17 +1879,8 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_augmented_op(assignment, target_type, value_type) } - fn infer_type_alias_statement(&mut self, type_alias_statement: &ast::StmtTypeAlias) { - let ast::StmtTypeAlias { - range: _, - name, - type_params: _, - value, - } = type_alias_statement; - self.infer_expression(value); - self.infer_expression(name); - - // TODO: properly handle generic type aliases, which need their own annotation scope + fn infer_type_alias_statement(&mut self, node: &ast::StmtTypeAlias) { + self.infer_definition(node); } fn infer_for_statement(&mut self, for_statement: &ast::StmtFor) { @@ -4578,7 +4618,14 @@ impl<'db> TypeInferenceBuilder<'db> { } _ => self.infer_type_expression(parameters), }, - KnownInstanceType::TypeVar(_) => todo_type!(), + KnownInstanceType::TypeVar(_) => { + self.infer_type_expression(parameters); + todo_type!() + } + KnownInstanceType::TypeAliasType(_) => { + self.infer_type_expression(parameters); + todo_type!("generic type alias") + } } } diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index 5d37617fa8..e31d487bdf 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -372,6 +372,7 @@ impl<'db> ClassBase<'db> { | Type::SubclassOf(_) => None, Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeVar(_) + | KnownInstanceType::TypeAliasType(_) | KnownInstanceType::Literal | KnownInstanceType::Union | KnownInstanceType::Optional => None, diff --git a/crates/red_knot_workspace/resources/test/corpus/89_type_alias_invalid_bound.py b/crates/red_knot_workspace/resources/test/corpus/89_type_alias_invalid_bound.py new file mode 120000 index 0000000000..5dfcadebb0 --- /dev/null +++ b/crates/red_knot_workspace/resources/test/corpus/89_type_alias_invalid_bound.py @@ -0,0 +1 @@ +../../../../ruff_python_parser/resources/inline/err/type_param_invalid_bound_expr.py \ No newline at end of file diff --git a/crates/red_knot_workspace/tests/check.rs b/crates/red_knot_workspace/tests/check.rs index 0a2ad2f426..ba90a0d1de 100644 --- a/crates/red_knot_workspace/tests/check.rs +++ b/crates/red_knot_workspace/tests/check.rs @@ -264,26 +264,16 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> { } /// Whether or not the .py/.pyi version of this file is expected to fail +#[rustfmt::skip] const KNOWN_FAILURES: &[(&str, bool, bool)] = &[ - // Probably related to missing support for type aliases / type params: - ("crates/ruff_python_parser/resources/inline/err/type_param_invalid_bound_expr.py", true, true), - ("crates/ruff_python_parser/resources/inline/err/type_param_param_spec_invalid_default_expr.py", true, true), - ("crates/ruff_python_parser/resources/inline/err/type_param_type_var_invalid_default_expr.py", true, true), - ("crates/ruff_python_parser/resources/inline/err/type_param_type_var_missing_default.py", true, true), - ("crates/ruff_python_parser/resources/inline/err/type_param_type_var_tuple_invalid_default_expr.py", true, true), - ("crates/ruff_python_parser/resources/inline/ok/type_param_param_spec.py", true, true), - ("crates/ruff_python_parser/resources/inline/ok/type_param_type_var_tuple.py", true, true), - ("crates/ruff_python_parser/resources/inline/ok/type_param_type_var.py", true, true), - ("crates/ruff_python_parser/resources/valid/statement/type.py", true, true), - ("crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_15.py", true, true), - ("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_19.py", true, true), - ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true), - ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true), - ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_17.py", true, true), - ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_20.py", true, true), + // related to circular references in class definitions ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false), - // Fails for unknown reasons: - ("crates/ruff_linter/resources/test/fixtures/pyflakes/F632.py", true, true), ("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false), ("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false), + // related to circular references in type aliases (salsa cycle panic): + ("crates/ruff_python_parser/resources/inline/err/type_alias_invalid_value_expr.py", true, true), + // related to string annotations (https://github.com/astral-sh/ruff/issues/14440) + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true), + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true), + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F632.py", true, true), ];