From 6dcbda89078685e279b5c5efb8e3dc4adc904cad Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 7 Nov 2025 20:39:23 -0500 Subject: [PATCH 01/11] [ty] Add diagnostics for `isinstance()` and `issubclass()` calls that use invalid PEP-604 unions for their second argument --- .../resources/mdtest/narrow/isinstance.md | 15 +++- .../resources/mdtest/narrow/issubclass.md | 5 +- ...an_in\342\200\246_(eeef56c0ef87a30b).snap" | 88 +++++++++++++++++++ ...an_in\342\200\246_(7bb66a0f412caac1).snap" | 42 +++++++++ .../ty_python_semantic/src/types/function.rs | 71 ++++++++++++++- 5 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins\342\200\246_-_`classinfo`_is_an_in\342\200\246_(eeef56c0ef87a30b).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub\342\200\246_-_`classinfo`_is_an_in\342\200\246_(7bb66a0f412caac1).snap" diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 0d3e11b996807..48df6acd30db7 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -104,16 +104,25 @@ Except for the `None` special case mentioned above, narrowing can only take plac the PEP-604 union are class literals. If any elements are generic aliases or other types, the `isinstance()` call may fail at runtime, so no narrowing can take place: + + ```toml [environment] python-version = "3.10" ``` ```py +from typing import Any, Literal, NamedTuple + def _(x: int | list[int] | bytes): - # TODO: this fails at runtime; we should emit a diagnostic - # (requires special-casing of the `isinstance()` signature) - if isinstance(x, int | list[int]): + # error: [invalid-argument-type] + if isinstance(x, list[int] | int): + reveal_type(x) # revealed: int | list[int] | bytes + # error: [invalid-argument-type] + elif isinstance(x, Literal[42] | list[int] | bytes): + reveal_type(x) # revealed: int | list[int] | bytes + # error: [invalid-argument-type] + elif isinstance(x, Any | NamedTuple | list[int]): reveal_type(x) # revealed: int | list[int] | bytes else: reveal_type(x) # revealed: int | list[int] | bytes diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index 11eb2ebaf4e02..139c4798436a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -165,6 +165,8 @@ Except for the `None` special case mentioned above, narrowing can only take plac the PEP-604 union are class literals. If any elements are generic aliases or other types, the `issubclass()` call may fail at runtime, so no narrowing can take place: + + ```toml [environment] python-version = "3.10" @@ -172,8 +174,7 @@ python-version = "3.10" ```py def _(x: type[int | list | bytes]): - # TODO: this fails at runtime; we should emit a diagnostic - # (requires special-casing of the `issubclass()` signature) + # error: [invalid-argument-type] if issubclass(x, int | list[int]): reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] else: diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins\342\200\246_-_`classinfo`_is_an_in\342\200\246_(eeef56c0ef87a30b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins\342\200\246_-_`classinfo`_is_an_in\342\200\246_(eeef56c0ef87a30b).snap" new file mode 100644 index 0000000000000..34383c8fd0962 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins\342\200\246_-_`classinfo`_is_an_in\342\200\246_(eeef56c0ef87a30b).snap" @@ -0,0 +1,88 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: isinstance.md - Narrowing for `isinstance` checks - `classinfo` is an invalid PEP-604 union of types +mdtest path: crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Any, Literal, NamedTuple + 2 | + 3 | def _(x: int | list[int] | bytes): + 4 | # error: [invalid-argument-type] + 5 | if isinstance(x, list[int] | int): + 6 | reveal_type(x) # revealed: int | list[int] | bytes + 7 | # error: [invalid-argument-type] + 8 | elif isinstance(x, Literal[42] | list[int] | bytes): + 9 | reveal_type(x) # revealed: int | list[int] | bytes +10 | # error: [invalid-argument-type] +11 | elif isinstance(x, Any | NamedTuple | list[int]): +12 | reveal_type(x) # revealed: int | list[int] | bytes +13 | else: +14 | reveal_type(x) # revealed: int | list[int] | bytes +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Invalid second argument to `isinstance` + --> src/mdtest_snippet.py:5:8 + | +3 | def _(x: int | list[int] | bytes): +4 | # error: [invalid-argument-type] +5 | if isinstance(x, list[int] | int): + | ^^^^^^^^^^^^^^---------------^ + | | + | This `UnionType` instance contains non-class elements +6 | reveal_type(x) # revealed: int | list[int] | bytes +7 | # error: [invalid-argument-type] + | +info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects +info: Element `` in the union is not a class object +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Invalid second argument to `isinstance` + --> src/mdtest_snippet.py:8:10 + | + 6 | reveal_type(x) # revealed: int | list[int] | bytes + 7 | # error: [invalid-argument-type] + 8 | elif isinstance(x, Literal[42] | list[int] | bytes): + | ^^^^^^^^^^^^^^-------------------------------^ + | | + | This `UnionType` instance contains non-class elements + 9 | reveal_type(x) # revealed: int | list[int] | bytes +10 | # error: [invalid-argument-type] + | +info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects +info: Elements `typing.Literal` and `` in the union are not class objects +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Invalid second argument to `isinstance` + --> src/mdtest_snippet.py:11:10 + | + 9 | reveal_type(x) # revealed: int | list[int] | bytes +10 | # error: [invalid-argument-type] +11 | elif isinstance(x, Any | NamedTuple | list[int]): + | ^^^^^^^^^^^^^^----------------------------^ + | | + | This `UnionType` instance contains non-class elements +12 | reveal_type(x) # revealed: int | list[int] | bytes +13 | else: + | +info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects +info: Element `typing.Any` in the union, and 2 more elements, are not class objects +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub\342\200\246_-_`classinfo`_is_an_in\342\200\246_(7bb66a0f412caac1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub\342\200\246_-_`classinfo`_is_an_in\342\200\246_(7bb66a0f412caac1).snap" new file mode 100644 index 0000000000000..27318dfe2bd21 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub\342\200\246_-_`classinfo`_is_an_in\342\200\246_(7bb66a0f412caac1).snap" @@ -0,0 +1,42 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: issubclass.md - Narrowing for `issubclass` checks - `classinfo` is an invalid PEP-604 union of types +mdtest path: crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def _(x: type[int | list | bytes]): +2 | # error: [invalid-argument-type] +3 | if issubclass(x, int | list[int]): +4 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] +5 | else: +6 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Invalid second argument to `issubclass` + --> src/mdtest_snippet.py:3:8 + | +1 | def _(x: type[int | list | bytes]): +2 | # error: [invalid-argument-type] +3 | if issubclass(x, int | list[int]): + | ^^^^^^^^^^^^^^---------------^ + | | + | This `UnionType` instance contains non-class elements +4 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] +5 | else: + | +info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects +info: Element `` in the union is not a class object +info: rule `invalid-argument-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 6244b0a85afa5..2462748d03f0c 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -81,9 +81,9 @@ use crate::types::visitor::any_over_type; use crate::types::{ ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, NormalizedVisitor, - SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation, UnionBuilder, - binding_type, todo_type, walk_signature, + HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, + NormalizedVisitor, SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation, + UnionBuilder, binding_type, todo_type, walk_signature, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -1755,6 +1755,71 @@ impl KnownFunction { diagnostic .set_primary_message("This call will raise `TypeError` at runtime"); } + + Type::KnownInstance(KnownInstanceType::UnionType(_)) => { + fn find_invalid_elements<'db>( + db: &'db dyn Db, + ty: Type<'db>, + invalid_elements: &mut Vec>, + ) { + match ty { + Type::ClassLiteral(_) => {} + Type::NominalInstance(instance) + if instance.has_known_class(db, KnownClass::NoneType) => {} + Type::KnownInstance(KnownInstanceType::UnionType(union)) => { + for element in union.elements(db) { + find_invalid_elements(db, *element, invalid_elements); + } + } + _ => invalid_elements.push(ty), + } + } + + let mut invalid_elements = vec![]; + find_invalid_elements(db, *second_argument, &mut invalid_elements); + + let Some((first_invalid_element, other_invalid_elements)) = + invalid_elements.split_first() + else { + return; + }; + + let Some(builder) = + context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression) + else { + return; + }; + + let function_name: &str = self.into(); + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid second argument to `{function_name}`" + )); + diagnostic.info(format_args!( + "A `UnionType` instance can only be used as the second argument to \ + `{function_name}` if all elements are class objects" + )); + diagnostic.annotate( + Annotation::secondary(context.span(&call_expression.arguments.args[1])) + .message("This `UnionType` instance contains non-class elements"), + ); + match other_invalid_elements { + [] => diagnostic.info(format_args!( + "Element `{}` in the union is not a class object", + first_invalid_element.display(db) + )), + [single] => diagnostic.info(format_args!( + "Elements `{}` and `{}` in the union are not class objects", + first_invalid_element.display(db), + single.display(db), + )), + _ => diagnostic.info(format_args!( + "Element `{}` in the union, and {} more elements, are not class objects", + first_invalid_element.display(db), + other_invalid_elements.len(), + )) + } + } _ => {} } } From 66a3d3dbc29540ef8c1a0699d1a288333231ad33 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 9 Nov 2025 21:49:26 +0000 Subject: [PATCH 02/11] refactor type-expression parsing for special forms to use a new `SpecialFormCategory` enum --- .../src/types/diagnostic.rs | 3 +- .../types/infer/builder/type_expression.rs | 289 ++++++++---------- .../src/types/special_form.rs | 242 +++++++++++++++ 3 files changed, 373 insertions(+), 161 deletions(-) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 33efcc74fd526..67a5789b29639 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -2574,7 +2574,7 @@ pub(crate) fn report_invalid_arguments_to_annotated( pub(crate) fn report_invalid_argument_number_to_special_form( context: &InferContext, subscript: &ast::ExprSubscript, - special_form: SpecialFormType, + special_form: impl Into, received_arguments: usize, expected_arguments: u8, ) { @@ -2587,6 +2587,7 @@ pub(crate) fn report_invalid_argument_number_to_special_form( builder.into_diagnostic(format_args!( "Special form `{special_form}` expected exactly {expected_arguments} {noun}, \ got {received_arguments}", + special_form = special_form.into(), )); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index d091487ce7543..0e3447775ae40 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -7,6 +7,7 @@ use crate::types::diagnostic::{ report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable, }; use crate::types::signatures::Signature; +use crate::types::special_form::{self, AliasSpec, LegacyStdlibAlias}; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::visitor::any_over_type; @@ -866,9 +867,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fn infer_parameterized_legacy_typing_alias( &mut self, subscript_node: &ast::ExprSubscript, - expected_arg_count: usize, - alias: SpecialFormType, - class: KnownClass, + alias: LegacyStdlibAlias, ) -> Type<'db> { let arguments = &*subscript_node.slice; let args = if let ast::Expr::Tuple(t) = arguments { @@ -876,15 +875,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { std::slice::from_ref(arguments) }; - if args.len() != expected_arg_count { + + let AliasSpec { + class, + expected_argument_number, + } = alias.alias_spec(); + + if args.len() != expected_argument_number { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript_node) { - let noun = if expected_arg_count == 1 { + let noun = if expected_argument_number == 1 { "argument" } else { "arguments" }; builder.into_diagnostic(format_args!( - "Legacy alias `{alias}` expected exactly {expected_arg_count} {noun}, \ + "Legacy alias `{alias}` expected exactly {expected_argument_number} {noun}, \ got {}", args.len() )); @@ -900,15 +905,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ty } - fn infer_parameterized_special_form_type_expression( + fn infer_parameterized_non_stdlib_alias_special_form( &mut self, subscript: &ast::ExprSubscript, - special_form: SpecialFormType, + special_form: special_form::NonStdlibAlias, ) -> Type<'db> { let db = self.db(); let arguments_slice = &*subscript.slice; + match special_form { - SpecialFormType::Annotated => { + special_form::NonStdlibAlias::Annotated => { let ast::Expr::Tuple(ast::ExprTuple { elts: arguments, .. }) = arguments_slice @@ -942,27 +948,29 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.store_expression_type(arguments_slice, ty); ty } - SpecialFormType::Literal => match self.infer_literal_parameter_type(arguments_slice) { - Ok(ty) => ty, - Err(nodes) => { - for node in nodes { - let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) - else { - continue; - }; - builder.into_diagnostic( - "Type arguments for `Literal` must be `None`, \ + special_form::NonStdlibAlias::Literal => { + match self.infer_literal_parameter_type(arguments_slice) { + Ok(ty) => ty, + Err(nodes) => { + for node in nodes { + let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) + else { + continue; + }; + builder.into_diagnostic( + "Type arguments for `Literal` must be `None`, \ a literal value (int, bool, str, or bytes), or an enum member", - ); + ); + } + Type::unknown() } - Type::unknown() } - }, - SpecialFormType::Optional => { + } + special_form::NonStdlibAlias::Optional => { let param_type = self.infer_type_expression(arguments_slice); UnionType::from_elements_leave_aliases(db, [param_type, Type::none(db)]) } - SpecialFormType::Union => match arguments_slice { + special_form::NonStdlibAlias::Union => match arguments_slice { ast::Expr::Tuple(t) => { let union_ty = UnionType::from_elements_leave_aliases( db, @@ -973,56 +981,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } _ => self.infer_type_expression(arguments_slice), }, - SpecialFormType::Callable => { - let mut arguments = match arguments_slice { - ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), - _ => { - self.infer_callable_parameter_types(arguments_slice); - Either::Right(std::iter::empty::<&ast::Expr>()) - } - }; - - let first_argument = arguments.next(); - - let parameters = - first_argument.and_then(|arg| self.infer_callable_parameter_types(arg)); - - let return_type = arguments.next().map(|arg| self.infer_type_expression(arg)); - - let correct_argument_number = if let Some(third_argument) = arguments.next() { - self.infer_type_expression(third_argument); - for argument in arguments { - self.infer_type_expression(argument); - } - false - } else { - return_type.is_some() - }; - - if !correct_argument_number { - report_invalid_arguments_to_callable(&self.context, subscript); - } - - let callable_type = if let (Some(parameters), Some(return_type), true) = - (parameters, return_type, correct_argument_number) - { - CallableType::single(db, Signature::new(parameters, Some(return_type))) - } else { - CallableType::unknown(db) - }; - - // `Signature` / `Parameters` are not a `Type` variant, so we're storing - // the outer callable type on these expressions instead. - self.store_expression_type(arguments_slice, callable_type); - if let Some(first_argument) = first_argument { - self.store_expression_type(first_argument, callable_type); - } - - callable_type - } // `ty_extensions` special forms - SpecialFormType::Not => { + special_form::NonStdlibAlias::Not => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1049,7 +1010,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } negated_type } - SpecialFormType::Intersection => { + special_form::NonStdlibAlias::Intersection => { let elements = match arguments_slice { ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), element => Either::Right(std::iter::once(element)), @@ -1066,7 +1027,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ty } - SpecialFormType::Top => { + special_form::NonStdlibAlias::Top => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1090,7 +1051,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; arg.top_materialization(db) } - SpecialFormType::Bottom => { + special_form::NonStdlibAlias::Bottom => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1114,7 +1075,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; arg.bottom_materialization(db) } - SpecialFormType::TypeOf => { + special_form::NonStdlibAlias::TypeOf => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1143,7 +1104,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { type_of_type } - SpecialFormType::CallableTypeOf => { + special_form::NonStdlibAlias::CallableTypeOf => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1193,67 +1154,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } callable_type } - - SpecialFormType::ChainMap => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::ChainMap, - KnownClass::ChainMap, - ), - SpecialFormType::OrderedDict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::OrderedDict, - KnownClass::OrderedDict, - ), - SpecialFormType::Dict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::Dict, - KnownClass::Dict, - ), - SpecialFormType::List => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::List, - KnownClass::List, - ), - SpecialFormType::DefaultDict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::DefaultDict, - KnownClass::DefaultDict, - ), - SpecialFormType::Counter => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Counter, - KnownClass::Counter, - ), - SpecialFormType::Set => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Set, - KnownClass::Set, - ), - SpecialFormType::FrozenSet => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::FrozenSet, - KnownClass::FrozenSet, - ), - SpecialFormType::Deque => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Deque, - KnownClass::Deque, - ), - - SpecialFormType::ClassVar - | SpecialFormType::Final - | SpecialFormType::Required - | SpecialFormType::NotRequired - | SpecialFormType::ReadOnly => { + special_form::NonStdlibAlias::ClassVar + | special_form::NonStdlibAlias::Final + | special_form::NonStdlibAlias::Required + | special_form::NonStdlibAlias::NotRequired + | special_form::NonStdlibAlias::ReadOnly => { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let diag = builder.into_diagnostic(format_args!( "Type qualifier `{special_form}` is not allowed in type expressions \ @@ -1263,14 +1168,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } self.infer_type_expression(arguments_slice) } - SpecialFormType::TypeIs => match arguments_slice { + special_form::NonStdlibAlias::TypeIs => match arguments_slice { ast::Expr::Tuple(_) => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let diag = builder.into_diagnostic(format_args!( - "Special form `{}` expected exactly one type parameter", - special_form.repr() + "Special form `{special_form}` expected exactly one type parameter", )); diagnostic::add_type_expression_reference_link(diag); } @@ -1289,11 +1193,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .top_materialization(self.db()), ), }, - SpecialFormType::TypeGuard => { + special_form::NonStdlibAlias::TypeGuard => { self.infer_type_expression(arguments_slice); todo_type!("`TypeGuard[]` special form") } - SpecialFormType::Concatenate => { + special_form::NonStdlibAlias::Concatenate => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1318,14 +1222,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } inferred_type } - SpecialFormType::Unpack => { + special_form::NonStdlibAlias::Unpack => { self.infer_type_expression(arguments_slice); todo_type!("`Unpack[]` special form") } - SpecialFormType::NoReturn - | SpecialFormType::Never - | SpecialFormType::AlwaysTruthy - | SpecialFormType::AlwaysFalsy => { + special_form::NonStdlibAlias::NoReturn + | special_form::NonStdlibAlias::Never + | special_form::NonStdlibAlias::AlwaysTruthy + | special_form::NonStdlibAlias::AlwaysFalsy => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1335,12 +1239,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - SpecialFormType::TypingSelf - | SpecialFormType::TypeAlias - | SpecialFormType::TypedDict - | SpecialFormType::Unknown - | SpecialFormType::Any - | SpecialFormType::NamedTuple => { + special_form::NonStdlibAlias::TypingSelf + | special_form::NonStdlibAlias::TypeAlias + | special_form::NonStdlibAlias::TypedDict + | special_form::NonStdlibAlias::Unknown + | special_form::NonStdlibAlias::Any + | special_form::NonStdlibAlias::NamedTuple => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1350,7 +1254,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - SpecialFormType::LiteralString => { + special_form::NonStdlibAlias::LiteralString => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1361,11 +1265,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - SpecialFormType::Type => self.infer_subclass_of_type_expression(arguments_slice), - SpecialFormType::Tuple => { - Type::tuple(self.infer_tuple_type_expression(arguments_slice)) - } - SpecialFormType::Generic | SpecialFormType::Protocol => { + special_form::NonStdlibAlias::Generic | special_form::NonStdlibAlias::Protocol => { self.infer_expression(arguments_slice, TypeContext::default()); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( @@ -1377,6 +1277,75 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } + fn infer_callable_type_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + let mut arguments = match &*subscript.slice { + ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), + _ => { + self.infer_callable_parameter_types(&subscript.slice); + Either::Right(std::iter::empty::<&ast::Expr>()) + } + }; + + let first_argument = arguments.next(); + let parameters = first_argument.and_then(|arg| self.infer_callable_parameter_types(arg)); + let return_type = arguments.next().map(|arg| self.infer_type_expression(arg)); + + let correct_argument_number = if let Some(third_argument) = arguments.next() { + self.infer_type_expression(third_argument); + for argument in arguments { + self.infer_type_expression(argument); + } + false + } else { + return_type.is_some() + }; + + if !correct_argument_number { + report_invalid_arguments_to_callable(&self.context, subscript); + } + + let callable_type = if let (Some(parameters), Some(return_type), true) = + (parameters, return_type, correct_argument_number) + { + CallableType::single(self.db(), Signature::new(parameters, Some(return_type))) + } else { + CallableType::unknown(self.db()) + }; + + // `Signature` / `Parameters` are not a `Type` variant, so we're storing + // the outer callable type on these expressions instead. + self.store_expression_type(&subscript.slice, callable_type); + if let Some(first_argument) = first_argument { + self.store_expression_type(first_argument, callable_type); + } + + callable_type + } + + fn infer_parameterized_special_form_type_expression( + &mut self, + subscript: &ast::ExprSubscript, + special_form: SpecialFormType, + ) -> Type<'db> { + match special_form.kind() { + special_form::SpecialFormCategory::LegacyStdlibAlias(alias) => { + self.infer_parameterized_legacy_typing_alias(subscript, alias) + } + special_form::SpecialFormCategory::NonStdlibAlias(alias) => { + self.infer_parameterized_non_stdlib_alias_special_form(subscript, alias) + } + special_form::SpecialFormCategory::Tuple => { + Type::tuple(self.infer_tuple_type_expression(&subscript.slice)) + } + special_form::SpecialFormCategory::Type => { + self.infer_subclass_of_type_expression(&subscript.slice) + } + special_form::SpecialFormCategory::Callable => { + self.infer_callable_type_expression(subscript) + } + } + } + pub(crate) fn infer_literal_parameter_type<'param>( &mut self, parameters: &'param ast::Expr, diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 721def0dee196..a2ee0502bf905 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -377,6 +377,69 @@ impl SpecialFormType { SpecialFormType::NamedTuple => "typing.NamedTuple", } } + + pub(super) const fn kind(self) -> SpecialFormCategory { + match self { + // See the `SpecialFormCategory` doc-comment for why these three are + // treated as their own category. + Self::Callable => SpecialFormCategory::Callable, + Self::Tuple => SpecialFormCategory::Tuple, + Self::Type => SpecialFormCategory::Type, + + // Legacy standard library aliases + Self::List => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::List), + Self::Dict => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Dict), + Self::Set => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Set), + Self::FrozenSet => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet), + Self::ChainMap => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap), + Self::Counter => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Counter), + Self::Deque => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Deque), + Self::DefaultDict => { + SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) + } + Self::OrderedDict => { + SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) + } + + // Non-standard-library aliases + Self::AlwaysFalsy => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::AlwaysFalsy), + Self::Unknown => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Unknown), + Self::Not => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Not), + Self::TypeOf => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypeOf), + Self::Top => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Top), + Self::Bottom => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Bottom), + Self::Annotated => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Annotated), + Self::Any => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Any), + Self::Literal => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Literal), + Self::Optional => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Optional), + Self::Union => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Union), + Self::NoReturn => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::NoReturn), + Self::Never => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Never), + Self::Final => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Final), + Self::ClassVar => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::ClassVar), + Self::Concatenate => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Concatenate), + Self::Unpack => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Unpack), + Self::Required => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Required), + Self::NotRequired => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::NotRequired), + Self::TypeAlias => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypeAlias), + Self::TypeGuard => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypeGuard), + Self::TypedDict => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypedDict), + Self::TypeIs => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypeIs), + Self::ReadOnly => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::ReadOnly), + Self::Protocol => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Protocol), + Self::Generic => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Generic), + Self::NamedTuple => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::NamedTuple), + Self::AlwaysTruthy => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::AlwaysTruthy), + Self::Intersection => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Intersection), + Self::TypingSelf => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypingSelf), + Self::LiteralString => { + SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::LiteralString) + } + Self::CallableTypeOf => { + SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::CallableTypeOf) + } + } + } } impl std::fmt::Display for SpecialFormType { @@ -384,3 +447,182 @@ impl std::fmt::Display for SpecialFormType { f.write_str(self.repr()) } } + +/// Various categories of special forms. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum SpecialFormCategory { + /// Special forms that are simple aliases to classes elsewhere in the standard library. + LegacyStdlibAlias(LegacyStdlibAlias), + + /// Special forms that are not aliases to classes elsewhere in the standard library. + NonStdlibAlias(NonStdlibAlias), + + /// The special form `typing.Tuple`. + /// + /// While this is technically an alias to `builtins.tuple`, it requires special handling + /// for type-expression parsing. + Tuple, + + /// The special form `typing.Type`. + /// + /// While this is technically an alias to `builtins.type`, it requires special handling + /// for type-expression parsing. + Type, + + /// The special form `Callable`. + /// + /// While `typing.Callable` aliases `collections.abc.Callable`, we view both objects + /// as inhabiting the same special form type internally. Moreover, `Callable` requires + /// special handling for both type-expression parsing and `isinstance`/`issubclass` + /// narrowing. + Callable, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum LegacyStdlibAlias { + List, + Dict, + Set, + FrozenSet, + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, +} + +impl LegacyStdlibAlias { + pub(super) const fn alias_spec(self) -> AliasSpec { + let (class, expected_argument_number) = match self { + LegacyStdlibAlias::List => (KnownClass::List, 1), + LegacyStdlibAlias::Dict => (KnownClass::Dict, 2), + LegacyStdlibAlias::Set => (KnownClass::Set, 1), + LegacyStdlibAlias::FrozenSet => (KnownClass::FrozenSet, 1), + LegacyStdlibAlias::ChainMap => (KnownClass::ChainMap, 2), + LegacyStdlibAlias::Counter => (KnownClass::Counter, 1), + LegacyStdlibAlias::DefaultDict => (KnownClass::DefaultDict, 2), + LegacyStdlibAlias::Deque => (KnownClass::Deque, 1), + LegacyStdlibAlias::OrderedDict => (KnownClass::OrderedDict, 2), + }; + + AliasSpec { + class, + expected_argument_number, + } + } + + pub(super) const fn aliased_class(self) -> KnownClass { + self.alias_spec().class + } +} + +impl From for SpecialFormType { + fn from(value: LegacyStdlibAlias) -> Self { + match value { + LegacyStdlibAlias::List => SpecialFormType::List, + LegacyStdlibAlias::Dict => SpecialFormType::Dict, + LegacyStdlibAlias::Set => SpecialFormType::Set, + LegacyStdlibAlias::FrozenSet => SpecialFormType::FrozenSet, + LegacyStdlibAlias::ChainMap => SpecialFormType::ChainMap, + LegacyStdlibAlias::Counter => SpecialFormType::Counter, + LegacyStdlibAlias::DefaultDict => SpecialFormType::DefaultDict, + LegacyStdlibAlias::Deque => SpecialFormType::Deque, + LegacyStdlibAlias::OrderedDict => SpecialFormType::OrderedDict, + } + } +} + +impl std::fmt::Display for LegacyStdlibAlias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + SpecialFormType::from(*self).fmt(f) + } +} + +/// Information regarding the [`KnownClass`] a [`LegacyStdlibAlias`] refers to. +pub(super) struct AliasSpec { + pub(super) class: KnownClass, + pub(super) expected_argument_number: usize, +} + +/// Enumeration of special forms that are not aliases to classes or special constructs +/// elsewhere in the Python standard library. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum NonStdlibAlias { + Any, + Annotated, + Literal, + LiteralString, + Optional, + Union, + NoReturn, + Never, + Unknown, + AlwaysTruthy, + AlwaysFalsy, + Not, + Intersection, + TypeOf, + CallableTypeOf, + Top, + Bottom, + TypingSelf, + Final, + ClassVar, + Concatenate, + Unpack, + Required, + NotRequired, + TypeAlias, + TypeGuard, + TypedDict, + TypeIs, + ReadOnly, + Protocol, + Generic, + NamedTuple, +} + +impl From for SpecialFormType { + fn from(value: NonStdlibAlias) -> Self { + match value { + NonStdlibAlias::Any => SpecialFormType::Any, + NonStdlibAlias::Annotated => SpecialFormType::Annotated, + NonStdlibAlias::Literal => SpecialFormType::Literal, + NonStdlibAlias::LiteralString => SpecialFormType::LiteralString, + NonStdlibAlias::Optional => SpecialFormType::Optional, + NonStdlibAlias::Union => SpecialFormType::Union, + NonStdlibAlias::NoReturn => SpecialFormType::NoReturn, + NonStdlibAlias::Never => SpecialFormType::Never, + NonStdlibAlias::Unknown => SpecialFormType::Unknown, + NonStdlibAlias::AlwaysTruthy => SpecialFormType::AlwaysTruthy, + NonStdlibAlias::AlwaysFalsy => SpecialFormType::AlwaysFalsy, + NonStdlibAlias::Not => SpecialFormType::Not, + NonStdlibAlias::Intersection => SpecialFormType::Intersection, + NonStdlibAlias::TypeOf => SpecialFormType::TypeOf, + NonStdlibAlias::CallableTypeOf => SpecialFormType::CallableTypeOf, + NonStdlibAlias::Top => SpecialFormType::Top, + NonStdlibAlias::Bottom => SpecialFormType::Bottom, + NonStdlibAlias::TypingSelf => SpecialFormType::TypingSelf, + NonStdlibAlias::Final => SpecialFormType::Final, + NonStdlibAlias::ClassVar => SpecialFormType::ClassVar, + NonStdlibAlias::Concatenate => SpecialFormType::Concatenate, + NonStdlibAlias::Unpack => SpecialFormType::Unpack, + NonStdlibAlias::Required => SpecialFormType::Required, + NonStdlibAlias::NotRequired => SpecialFormType::NotRequired, + NonStdlibAlias::TypeAlias => SpecialFormType::TypeAlias, + NonStdlibAlias::TypeGuard => SpecialFormType::TypeGuard, + NonStdlibAlias::TypedDict => SpecialFormType::TypedDict, + NonStdlibAlias::TypeIs => SpecialFormType::TypeIs, + NonStdlibAlias::ReadOnly => SpecialFormType::ReadOnly, + NonStdlibAlias::Protocol => SpecialFormType::Protocol, + NonStdlibAlias::Generic => SpecialFormType::Generic, + NonStdlibAlias::NamedTuple => SpecialFormType::NamedTuple, + } + } +} + +impl std::fmt::Display for NonStdlibAlias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + SpecialFormType::from(*self).fmt(f) + } +} From ece90cb7d6f29cc126d3e34815e4ce534503843c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 9 Nov 2025 21:58:09 +0000 Subject: [PATCH 03/11] Reuse `SpecialFormCategory` in `class_base.rs` --- .../src/types/class_base.rs | 143 ++++++++---------- 1 file changed, 62 insertions(+), 81 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index caddc885671b9..3a03d80b9b075 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,6 +1,7 @@ use crate::Db; use crate::types::class::CodeGeneratorKind; use crate::types::generics::Specialization; +use crate::types::special_form::{self, SpecialFormCategory}; use crate::types::tuple::TupleType; use crate::types::{ ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType, @@ -172,94 +173,74 @@ impl<'db> ClassBase<'db> { | KnownInstanceType::Literal(_) => None, }, - Type::SpecialForm(special_form) => match special_form { - SpecialFormType::Annotated - | SpecialFormType::Literal - | SpecialFormType::LiteralString - | SpecialFormType::Union - | SpecialFormType::NoReturn - | SpecialFormType::Never - | SpecialFormType::Final - | SpecialFormType::NotRequired - | SpecialFormType::TypeGuard - | SpecialFormType::TypeIs - | SpecialFormType::TypingSelf - | SpecialFormType::Unpack - | SpecialFormType::ClassVar - | SpecialFormType::Concatenate - | SpecialFormType::Required - | SpecialFormType::TypeAlias - | SpecialFormType::ReadOnly - | SpecialFormType::Optional - | SpecialFormType::Not - | SpecialFormType::Top - | SpecialFormType::Bottom - | SpecialFormType::Intersection - | SpecialFormType::TypeOf - | SpecialFormType::CallableTypeOf - | SpecialFormType::AlwaysTruthy - | SpecialFormType::AlwaysFalsy => None, - - SpecialFormType::Any => Some(Self::Dynamic(DynamicType::Any)), - SpecialFormType::Unknown => Some(Self::unknown()), - - SpecialFormType::Protocol => Some(Self::Protocol), - SpecialFormType::Generic => Some(Self::Generic), - - SpecialFormType::NamedTuple => { - let fields = subclass.own_fields(db, None, CodeGeneratorKind::NamedTuple); - Self::try_from_type( - db, - TupleType::heterogeneous( + Type::SpecialForm(special_form) => match special_form.kind() { + SpecialFormCategory::NonStdlibAlias(alias) => match alias { + special_form::NonStdlibAlias::Any => Some(Self::Dynamic(DynamicType::Any)), + special_form::NonStdlibAlias::Unknown => Some(Self::unknown()), + special_form::NonStdlibAlias::Protocol => Some(Self::Protocol), + special_form::NonStdlibAlias::Generic => Some(Self::Generic), + special_form::NonStdlibAlias::TypedDict => Some(Self::TypedDict), + + special_form::NonStdlibAlias::NamedTuple => { + let fields = subclass.own_fields(db, None, CodeGeneratorKind::NamedTuple); + Self::try_from_type( db, - fields.values().map(|field| field.declared_ty), - )? - .to_class_type(db) - .into(), - subclass, - ) + TupleType::heterogeneous( + db, + fields.values().map(|field| field.declared_ty), + )? + .to_class_type(db) + .into(), + subclass, + ) + } + + special_form::NonStdlibAlias::AlwaysFalsy + | special_form::NonStdlibAlias::AlwaysTruthy + | special_form::NonStdlibAlias::TypeOf + | special_form::NonStdlibAlias::CallableTypeOf + | special_form::NonStdlibAlias::TypeIs + | special_form::NonStdlibAlias::TypingSelf + | special_form::NonStdlibAlias::Not + | special_form::NonStdlibAlias::Top + | special_form::NonStdlibAlias::Bottom + | special_form::NonStdlibAlias::Intersection + | special_form::NonStdlibAlias::Literal + | special_form::NonStdlibAlias::LiteralString + | special_form::NonStdlibAlias::Annotated + | special_form::NonStdlibAlias::Final + | special_form::NonStdlibAlias::NotRequired + | special_form::NonStdlibAlias::Required + | special_form::NonStdlibAlias::TypeAlias + | special_form::NonStdlibAlias::ReadOnly + | special_form::NonStdlibAlias::Optional + | special_form::NonStdlibAlias::Unpack + | special_form::NonStdlibAlias::ClassVar + | special_form::NonStdlibAlias::Concatenate + | special_form::NonStdlibAlias::Never + | special_form::NonStdlibAlias::NoReturn + | special_form::NonStdlibAlias::Union + | special_form::NonStdlibAlias::TypeGuard => None, + }, + + SpecialFormCategory::LegacyStdlibAlias(alias) => { + Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass) } - // TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO - SpecialFormType::Dict => { - Self::try_from_type(db, KnownClass::Dict.to_class_literal(db), subclass) - } - SpecialFormType::List => { - Self::try_from_type(db, KnownClass::List.to_class_literal(db), subclass) - } - SpecialFormType::Type => { - Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) - } - SpecialFormType::Tuple => { - Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass) - } - SpecialFormType::Set => { - Self::try_from_type(db, KnownClass::Set.to_class_literal(db), subclass) - } - SpecialFormType::FrozenSet => { - Self::try_from_type(db, KnownClass::FrozenSet.to_class_literal(db), subclass) - } - SpecialFormType::ChainMap => { - Self::try_from_type(db, KnownClass::ChainMap.to_class_literal(db), subclass) - } - SpecialFormType::Counter => { - Self::try_from_type(db, KnownClass::Counter.to_class_literal(db), subclass) - } - SpecialFormType::DefaultDict => { - Self::try_from_type(db, KnownClass::DefaultDict.to_class_literal(db), subclass) - } - SpecialFormType::Deque => { - Self::try_from_type(db, KnownClass::Deque.to_class_literal(db), subclass) - } - SpecialFormType::OrderedDict => { - Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db), subclass) - } - SpecialFormType::TypedDict => Some(Self::TypedDict), - SpecialFormType::Callable => Self::try_from_type( + SpecialFormCategory::Callable => Self::try_from_type( db, todo_type!("Support for Callable as a base class"), subclass, ), + + SpecialFormCategory::Tuple => { + Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass) + } + + // TODO: Classes inheriting from `typing.Type` also have `Generic` in their MRO + SpecialFormCategory::Type => { + Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) + } }, } } From 382d0875c30e6dfe61a41d65692ce1de17daa2ac Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 9 Nov 2025 22:07:31 +0000 Subject: [PATCH 04/11] Reuse `SpecialFormCategory` in `Type::in_type_expression()` --- crates/ty_python_semantic/src/types.rs | 135 ++---------------- .../src/types/special_form.rs | 118 +++++++++++++++ 2 files changed, 130 insertions(+), 123 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e3b8b8e89e99c..98ac76ab305f3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1,4 +1,3 @@ -use infer::nearest_enclosing_class; use itertools::{Either, Itertools}; use ruff_db::parsed::parsed_module; @@ -60,13 +59,13 @@ use crate::types::function::{ }; pub(crate) use crate::types::generics::GenericContext; use crate::types::generics::{ - InferableTypeVars, PartialSpecialization, Specialization, bind_typevar, typing_self, - walk_generic_context, + InferableTypeVars, PartialSpecialization, Specialization, bind_typevar, walk_generic_context, }; use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{ParameterForm, walk_signature}; +use crate::types::special_form::SpecialFormCategory; use crate::types::tuple::{TupleSpec, TupleSpecBuilder}; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; pub use crate::types::variance::TypeVarVariance; @@ -6465,127 +6464,17 @@ impl<'db> Type<'db> { KnownInstanceType::Literal(list) => Ok(list.to_union(db)), }, - Type::SpecialForm(special_form) => match special_form { - SpecialFormType::Never | SpecialFormType::NoReturn => Ok(Type::Never), - SpecialFormType::LiteralString => Ok(Type::LiteralString), - SpecialFormType::Any => Ok(Type::any()), - SpecialFormType::Unknown => Ok(Type::unknown()), - SpecialFormType::AlwaysTruthy => Ok(Type::AlwaysTruthy), - SpecialFormType::AlwaysFalsy => Ok(Type::AlwaysFalsy), - + Type::SpecialForm(special_form) => match special_form.kind() { // We treat `typing.Type` exactly the same as `builtins.type`: - SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), - SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), - - // Legacy `typing` aliases - SpecialFormType::List => Ok(KnownClass::List.to_instance(db)), - SpecialFormType::Dict => Ok(KnownClass::Dict.to_instance(db)), - SpecialFormType::Set => Ok(KnownClass::Set.to_instance(db)), - SpecialFormType::FrozenSet => Ok(KnownClass::FrozenSet.to_instance(db)), - SpecialFormType::ChainMap => Ok(KnownClass::ChainMap.to_instance(db)), - SpecialFormType::Counter => Ok(KnownClass::Counter.to_instance(db)), - SpecialFormType::DefaultDict => Ok(KnownClass::DefaultDict.to_instance(db)), - SpecialFormType::Deque => Ok(KnownClass::Deque.to_instance(db)), - SpecialFormType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)), - - // TODO: Use an opt-in rule for a bare `Callable` - SpecialFormType::Callable => Ok(CallableType::unknown(db)), - - // Special case: `NamedTuple` in a type expression is understood to describe the type - // `tuple[object, ...] & `. - // This isn't very principled (since at runtime, `NamedTuple` is just a function), - // but it appears to be what users often expect, and it improves compatibility with - // other type checkers such as mypy. - // See conversation in https://github.com/astral-sh/ruff/pull/19915. - SpecialFormType::NamedTuple => Ok(IntersectionBuilder::new(db) - .positive_elements([ - Type::homogeneous_tuple(db, Type::object()), - KnownClass::NamedTupleLike.to_instance(db), - ]) - .build()), - - SpecialFormType::TypingSelf => { - let index = semantic_index(db, scope_id.file(db)); - let Some(class) = nearest_enclosing_class(db, index, scope_id) else { - return Err(InvalidTypeExpressionError { - fallback_type: Type::unknown(), - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::InvalidType(*self, scope_id) - ], - }); - }; - - Ok(typing_self(db, scope_id, typevar_binding_context, class).unwrap_or(*self)) - } - SpecialFormType::TypeAlias => Ok(Type::Dynamic(DynamicType::TodoTypeAlias)), - SpecialFormType::TypedDict => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::TypedDict - ], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Literal - | SpecialFormType::Union - | SpecialFormType::Intersection => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::RequiresArguments(*self) - ], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Protocol => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::Protocol - ], - fallback_type: Type::unknown(), - }), - SpecialFormType::Generic => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Optional - | SpecialFormType::Not - | SpecialFormType::Top - | SpecialFormType::Bottom - | SpecialFormType::TypeOf - | SpecialFormType::TypeIs - | SpecialFormType::TypeGuard - | SpecialFormType::Unpack - | SpecialFormType::CallableTypeOf => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::RequiresOneArgument(*self) - ], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Annotated | SpecialFormType::Concatenate => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::RequiresTwoArguments(*self) - ], - fallback_type: Type::unknown(), - }) + SpecialFormCategory::Type => Ok(KnownClass::Type.to_instance(db)), + SpecialFormCategory::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), + SpecialFormCategory::Callable => Ok(CallableType::unknown(db)), + SpecialFormCategory::LegacyStdlibAlias(alias) => { + Ok(alias.aliased_class().to_instance(db)) } - - SpecialFormType::ClassVar | SpecialFormType::Final => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::TypeQualifier(*special_form) - ], - fallback_type: Type::unknown(), - }) + SpecialFormCategory::NonStdlibAlias(alias) => { + alias.in_type_expression(db, scope_id, typevar_binding_context) } - - SpecialFormType::ReadOnly - | SpecialFormType::NotRequired - | SpecialFormType::Required => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::TypeQualifierRequiresOneArgument(*special_form) - ], - fallback_type: Type::unknown(), - }), }, Type::Union(union) => { @@ -8088,10 +7977,10 @@ enum InvalidTypeExpression<'db> { TypedDict, /// Type qualifiers are always invalid in *type expressions*, /// but these ones are okay with 0 arguments in *annotation expressions* - TypeQualifier(SpecialFormType), + TypeQualifier(special_form::NonStdlibAlias), /// Type qualifiers that are invalid in type expressions, /// and which would require exactly one argument even if they appeared in an annotation expression - TypeQualifierRequiresOneArgument(SpecialFormType), + TypeQualifierRequiresOneArgument(special_form::NonStdlibAlias), /// Some types are always invalid in type expressions InvalidType(Type<'db>, ScopeId<'db>), } diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index a2ee0502bf905..01de2d27818e7 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -4,6 +4,11 @@ use super::{ClassType, Type, class::KnownClass}; use crate::db::Db; use crate::module_resolver::{KnownModule, file_to_module}; +use crate::semantic_index::{definition::Definition, scope::ScopeId, semantic_index}; +use crate::types::{ + DynamicType, IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, + generics::typing_self, infer::nearest_enclosing_class, +}; use ruff_db::files::File; use std::str::FromStr; @@ -582,6 +587,113 @@ pub(super) enum NonStdlibAlias { NamedTuple, } +impl NonStdlibAlias { + pub(super) fn in_type_expression<'db>( + self, + db: &'db dyn Db, + scope_id: ScopeId<'db>, + typevar_binding_context: Option>, + ) -> Result, InvalidTypeExpressionError<'db>> { + match self { + Self::Never | Self::NoReturn => Ok(Type::Never), + Self::LiteralString => Ok(Type::LiteralString), + Self::Any => Ok(Type::any()), + Self::Unknown => Ok(Type::unknown()), + Self::AlwaysTruthy => Ok(Type::AlwaysTruthy), + Self::AlwaysFalsy => Ok(Type::AlwaysFalsy), + + // Special case: `NamedTuple` in a type expression is understood to describe the type + // `tuple[object, ...] & `. + // This isn't very principled (since at runtime, `NamedTuple` is just a function), + // but it appears to be what users often expect, and it improves compatibility with + // other type checkers such as mypy. + // See conversation in https://github.com/astral-sh/ruff/pull/19915. + Self::NamedTuple => Ok(IntersectionBuilder::new(db) + .positive_elements([ + Type::homogeneous_tuple(db, Type::object()), + KnownClass::NamedTupleLike.to_instance(db), + ]) + .build()), + + Self::TypingSelf => { + let index = semantic_index(db, scope_id.file(db)); + let Some(class) = nearest_enclosing_class(db, index, scope_id) else { + return Err(InvalidTypeExpressionError { + fallback_type: Type::unknown(), + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::InvalidType(self.into(), scope_id) + ], + }); + }; + + Ok( + typing_self(db, scope_id, typevar_binding_context, class) + .unwrap_or(self.into()), + ) + } + Self::TypeAlias => Ok(Type::Dynamic(DynamicType::TodoTypeAlias)), + Self::TypedDict => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::TypedDict], + fallback_type: Type::unknown(), + }), + + Self::Literal | Self::Union | Self::Intersection => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresArguments(self.into()) + ], + fallback_type: Type::unknown(), + }), + + Self::Protocol => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Protocol], + fallback_type: Type::unknown(), + }), + Self::Generic => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic], + fallback_type: Type::unknown(), + }), + + Self::Optional + | Self::Not + | Self::Top + | Self::Bottom + | Self::TypeOf + | Self::TypeIs + | Self::TypeGuard + | Self::Unpack + | Self::CallableTypeOf => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresOneArgument(self.into()) + ], + fallback_type: Type::unknown(), + }), + + Self::Annotated | Self::Concatenate => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresTwoArguments(self.into()) + ], + fallback_type: Type::unknown(), + }), + + Self::ClassVar | Self::Final => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::TypeQualifier(self) + ], + fallback_type: Type::unknown(), + }), + + Self::ReadOnly | Self::NotRequired | Self::Required => { + Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::TypeQualifierRequiresOneArgument(self) + ], + fallback_type: Type::unknown(), + }) + } + } + } +} + impl From for SpecialFormType { fn from(value: NonStdlibAlias) -> Self { match value { @@ -621,6 +733,12 @@ impl From for SpecialFormType { } } +impl From for Type<'_> { + fn from(value: NonStdlibAlias) -> Self { + Type::SpecialForm(SpecialFormType::from(value)) + } +} + impl std::fmt::Display for NonStdlibAlias { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { SpecialFormType::from(*self).fmt(f) From e186625756f7541214d8d165b458ed57a3a9a88f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 9 Nov 2025 22:59:08 +0000 Subject: [PATCH 05/11] Add `SpecialFormCategory::TypeQualifier` and use it to simplify logic around type qualifiers --- crates/ty_python_semantic/src/types.rs | 27 ++- .../src/types/class_base.rs | 95 ++++---- .../infer/builder/annotation_expression.rs | 151 +++++------- .../types/infer/builder/type_expression.rs | 81 ++++--- .../src/types/special_form.rs | 218 ++++++++++-------- 5 files changed, 286 insertions(+), 286 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 98ac76ab305f3..de30b8b493f42 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -65,7 +65,7 @@ use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{ParameterForm, walk_signature}; -use crate::types::special_form::SpecialFormCategory; +use crate::types::special_form::{SpecialFormCategory, TypeQualifier}; use crate::types::tuple::{TupleSpec, TupleSpecBuilder}; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; pub use crate::types::variance::TypeVarVariance; @@ -6472,9 +6472,25 @@ impl<'db> Type<'db> { SpecialFormCategory::LegacyStdlibAlias(alias) => { Ok(alias.aliased_class().to_instance(db)) } - SpecialFormCategory::NonStdlibAlias(alias) => { + SpecialFormCategory::Other(alias) => { alias.in_type_expression(db, scope_id, typevar_binding_context) } + SpecialFormCategory::TypeQualifier(qualifier) => { + let err = match qualifier { + TypeQualifier::Final | TypeQualifier::ClassVar => { + InvalidTypeExpression::TypeQualifier(qualifier) + } + TypeQualifier::ReadOnly + | TypeQualifier::NotRequired + | TypeQualifier::Required => { + InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) + } + }; + Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![err], + fallback_type: Type::unknown(), + }) + } }, Type::Union(union) => { @@ -7911,8 +7927,9 @@ impl<'db> TypeAndQualifiers<'db> { } /// Insert/add an additional type qualifier. - pub(crate) fn add_qualifier(&mut self, qualifier: TypeQualifiers) { + pub(crate) fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { self.qualifiers |= qualifier; + self } /// Return the set of type qualifiers. @@ -7977,10 +7994,10 @@ enum InvalidTypeExpression<'db> { TypedDict, /// Type qualifiers are always invalid in *type expressions*, /// but these ones are okay with 0 arguments in *annotation expressions* - TypeQualifier(special_form::NonStdlibAlias), + TypeQualifier(TypeQualifier), /// Type qualifiers that are invalid in type expressions, /// and which would require exactly one argument even if they appeared in an annotation expression - TypeQualifierRequiresOneArgument(special_form::NonStdlibAlias), + TypeQualifierRequiresOneArgument(TypeQualifier), /// Some types are always invalid in type expressions InvalidType(Type<'db>, ScopeId<'db>), } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 3a03d80b9b075..dddd469608e96 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -174,55 +174,6 @@ impl<'db> ClassBase<'db> { }, Type::SpecialForm(special_form) => match special_form.kind() { - SpecialFormCategory::NonStdlibAlias(alias) => match alias { - special_form::NonStdlibAlias::Any => Some(Self::Dynamic(DynamicType::Any)), - special_form::NonStdlibAlias::Unknown => Some(Self::unknown()), - special_form::NonStdlibAlias::Protocol => Some(Self::Protocol), - special_form::NonStdlibAlias::Generic => Some(Self::Generic), - special_form::NonStdlibAlias::TypedDict => Some(Self::TypedDict), - - special_form::NonStdlibAlias::NamedTuple => { - let fields = subclass.own_fields(db, None, CodeGeneratorKind::NamedTuple); - Self::try_from_type( - db, - TupleType::heterogeneous( - db, - fields.values().map(|field| field.declared_ty), - )? - .to_class_type(db) - .into(), - subclass, - ) - } - - special_form::NonStdlibAlias::AlwaysFalsy - | special_form::NonStdlibAlias::AlwaysTruthy - | special_form::NonStdlibAlias::TypeOf - | special_form::NonStdlibAlias::CallableTypeOf - | special_form::NonStdlibAlias::TypeIs - | special_form::NonStdlibAlias::TypingSelf - | special_form::NonStdlibAlias::Not - | special_form::NonStdlibAlias::Top - | special_form::NonStdlibAlias::Bottom - | special_form::NonStdlibAlias::Intersection - | special_form::NonStdlibAlias::Literal - | special_form::NonStdlibAlias::LiteralString - | special_form::NonStdlibAlias::Annotated - | special_form::NonStdlibAlias::Final - | special_form::NonStdlibAlias::NotRequired - | special_form::NonStdlibAlias::Required - | special_form::NonStdlibAlias::TypeAlias - | special_form::NonStdlibAlias::ReadOnly - | special_form::NonStdlibAlias::Optional - | special_form::NonStdlibAlias::Unpack - | special_form::NonStdlibAlias::ClassVar - | special_form::NonStdlibAlias::Concatenate - | special_form::NonStdlibAlias::Never - | special_form::NonStdlibAlias::NoReturn - | special_form::NonStdlibAlias::Union - | special_form::NonStdlibAlias::TypeGuard => None, - }, - SpecialFormCategory::LegacyStdlibAlias(alias) => { Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass) } @@ -241,6 +192,52 @@ impl<'db> ClassBase<'db> { SpecialFormCategory::Type => { Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) } + + SpecialFormCategory::TypeQualifier(_) => None, + + SpecialFormCategory::Other(alias) => match alias { + special_form::MiscSpecialForm::Any => Some(Self::Dynamic(DynamicType::Any)), + special_form::MiscSpecialForm::Unknown => Some(Self::unknown()), + special_form::MiscSpecialForm::Protocol => Some(Self::Protocol), + special_form::MiscSpecialForm::Generic => Some(Self::Generic), + special_form::MiscSpecialForm::TypedDict => Some(Self::TypedDict), + + special_form::MiscSpecialForm::NamedTuple => { + let fields = subclass.own_fields(db, None, CodeGeneratorKind::NamedTuple); + Self::try_from_type( + db, + TupleType::heterogeneous( + db, + fields.values().map(|field| field.declared_ty), + )? + .to_class_type(db) + .into(), + subclass, + ) + } + + special_form::MiscSpecialForm::AlwaysFalsy + | special_form::MiscSpecialForm::AlwaysTruthy + | special_form::MiscSpecialForm::TypeOf + | special_form::MiscSpecialForm::CallableTypeOf + | special_form::MiscSpecialForm::TypeIs + | special_form::MiscSpecialForm::TypingSelf + | special_form::MiscSpecialForm::Not + | special_form::MiscSpecialForm::Top + | special_form::MiscSpecialForm::Bottom + | special_form::MiscSpecialForm::Intersection + | special_form::MiscSpecialForm::Literal + | special_form::MiscSpecialForm::LiteralString + | special_form::MiscSpecialForm::Annotated + | special_form::MiscSpecialForm::TypeAlias + | special_form::MiscSpecialForm::Optional + | special_form::MiscSpecialForm::Unpack + | special_form::MiscSpecialForm::Concatenate + | special_form::MiscSpecialForm::Never + | special_form::MiscSpecialForm::NoReturn + | special_form::MiscSpecialForm::Union + | special_form::MiscSpecialForm::TypeGuard => None, + }, }, } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 1e954d461e87f..51d225ef0b5ca 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -48,32 +48,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { annotation: &ast::Expr, builder: &TypeInferenceBuilder<'db, '_>, ) -> TypeAndQualifiers<'db> { - match ty { - Type::SpecialForm(SpecialFormType::ClassVar) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::CLASS_VAR, - ), - Type::SpecialForm(SpecialFormType::Final) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::FINAL, - ), - Type::SpecialForm(SpecialFormType::Required) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::REQUIRED, - ), - Type::SpecialForm(SpecialFormType::NotRequired) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::NOT_REQUIRED, - ), - Type::SpecialForm(SpecialFormType::ReadOnly) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::READ_ONLY, - ), + let special_case = match ty { + Type::SpecialForm(special_form) => { + special_form.as_type_qualifier().map(|qualifier| { + TypeAndQualifiers::new( + Type::unknown(), + TypeOrigin::Declared, + TypeQualifiers::from(qualifier), + ) + }) + } Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => { if let Some(builder) = builder.context.report_lint(&INVALID_TYPE_FORM, annotation) @@ -81,13 +65,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder .into_diagnostic("`InitVar` may not be used without a type argument"); } - TypeAndQualifiers::new( + Some(TypeAndQualifiers::new( Type::unknown(), TypeOrigin::Declared, TypeQualifiers::INIT_VAR, - ) + )) } - _ => TypeAndQualifiers::declared( + _ => None, + }; + + special_case.unwrap_or_else(|| { + TypeAndQualifiers::declared( ty.in_type_expression( builder.db(), builder.scope(), @@ -100,8 +88,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.is_reachable(annotation), ) }), - ), - } + ) + }) } // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression @@ -159,12 +147,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let slice = &**slice; - match value_ty { + let special_case = match value_ty { Type::SpecialForm(SpecialFormType::Annotated) => { // This branch is similar to the corresponding branch in `infer_parameterized_special_form_type_expression`, but // `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly // differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`). - if let ast::Expr::Tuple(ast::ExprTuple { + let ty = if let ast::Expr::Tuple(ast::ExprTuple { elts: arguments, .. }) = slice { @@ -192,62 +180,39 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { report_invalid_arguments_to_annotated(&self.context, subscript); self.infer_annotation_expression_impl(slice) - } - } - Type::SpecialForm( - type_qualifier @ (SpecialFormType::ClassVar - | SpecialFormType::Final - | SpecialFormType::Required - | SpecialFormType::NotRequired - | SpecialFormType::ReadOnly), - ) => { - let arguments = if let ast::Expr::Tuple(tuple) = slice { - &*tuple.elts - } else { - std::slice::from_ref(slice) }; - let num_arguments = arguments.len(); - let type_and_qualifiers = if num_arguments == 1 { - let mut type_and_qualifiers = - self.infer_annotation_expression_impl(slice); - - match type_qualifier { - SpecialFormType::ClassVar => { - type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR); - } - SpecialFormType::Final => { - type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL); - } - SpecialFormType::Required => { - type_and_qualifiers.add_qualifier(TypeQualifiers::REQUIRED); - } - SpecialFormType::NotRequired => { - type_and_qualifiers.add_qualifier(TypeQualifiers::NOT_REQUIRED); + Some(ty) + } + Type::SpecialForm(special_form) => { + special_form.as_type_qualifier().map(|qualifier| { + let arguments = if let ast::Expr::Tuple(tuple) = slice { + &*tuple.elts + } else { + std::slice::from_ref(slice) + }; + let num_arguments = arguments.len(); + let type_and_qualifiers = if num_arguments == 1 { + self.infer_annotation_expression_impl(slice) + .with_qualifier(TypeQualifiers::from(qualifier)) + } else { + for element in arguments { + self.infer_annotation_expression_impl(element); } - SpecialFormType::ReadOnly => { - type_and_qualifiers.add_qualifier(TypeQualifiers::READ_ONLY); + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "Type qualifier `{qualifier}` expected exactly 1 argument, \ + got {num_arguments}", + )); } - _ => unreachable!(), + TypeAndQualifiers::declared(Type::unknown()) + }; + if slice.is_tuple_expr() { + self.store_expression_type(slice, type_and_qualifiers.inner_type()); } type_and_qualifiers - } else { - for element in arguments { - self.infer_annotation_expression_impl(element); - } - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "Type qualifier `{type_qualifier}` expected exactly 1 argument, \ - got {num_arguments}", - )); - } - TypeAndQualifiers::declared(Type::unknown()) - }; - if slice.is_tuple_expr() { - self.store_expression_type(slice, type_and_qualifiers.inner_type()); - } - type_and_qualifiers + }) } Type::ClassLiteral(class) if class.is_known(self.db(), KnownClass::InitVar) => { let arguments = if let ast::Expr::Tuple(tuple) = slice { @@ -257,10 +222,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let num_arguments = arguments.len(); let type_and_qualifiers = if num_arguments == 1 { - let mut type_and_qualifiers = - self.infer_annotation_expression_impl(slice); - type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR); - type_and_qualifiers + self.infer_annotation_expression_impl(slice) + .with_qualifier(TypeQualifiers::INIT_VAR) } else { for element in arguments { self.infer_annotation_expression_impl(element); @@ -278,12 +241,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if slice.is_tuple_expr() { self.store_expression_type(slice, type_and_qualifiers.inner_type()); } - type_and_qualifiers + Some(type_and_qualifiers) } - _ => TypeAndQualifiers::declared( + _ => None, + }; + + special_case.unwrap_or_else(|| { + TypeAndQualifiers::declared( self.infer_subscript_type_expression_no_store(subscript, slice, value_ty), - ), - } + ) + }) } // All other annotation expressions are (possibly) valid type expressions, so handle diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 0e3447775ae40..9a76d653c4038 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -908,13 +908,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fn infer_parameterized_non_stdlib_alias_special_form( &mut self, subscript: &ast::ExprSubscript, - special_form: special_form::NonStdlibAlias, + special_form: special_form::MiscSpecialForm, ) -> Type<'db> { let db = self.db(); let arguments_slice = &*subscript.slice; match special_form { - special_form::NonStdlibAlias::Annotated => { + special_form::MiscSpecialForm::Annotated => { let ast::Expr::Tuple(ast::ExprTuple { elts: arguments, .. }) = arguments_slice @@ -948,7 +948,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.store_expression_type(arguments_slice, ty); ty } - special_form::NonStdlibAlias::Literal => { + special_form::MiscSpecialForm::Literal => { match self.infer_literal_parameter_type(arguments_slice) { Ok(ty) => ty, Err(nodes) => { @@ -966,11 +966,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } } - special_form::NonStdlibAlias::Optional => { + special_form::MiscSpecialForm::Optional => { let param_type = self.infer_type_expression(arguments_slice); UnionType::from_elements_leave_aliases(db, [param_type, Type::none(db)]) } - special_form::NonStdlibAlias::Union => match arguments_slice { + special_form::MiscSpecialForm::Union => match arguments_slice { ast::Expr::Tuple(t) => { let union_ty = UnionType::from_elements_leave_aliases( db, @@ -983,7 +983,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }, // `ty_extensions` special forms - special_form::NonStdlibAlias::Not => { + special_form::MiscSpecialForm::Not => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1010,7 +1010,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } negated_type } - special_form::NonStdlibAlias::Intersection => { + special_form::MiscSpecialForm::Intersection => { let elements = match arguments_slice { ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), element => Either::Right(std::iter::once(element)), @@ -1027,7 +1027,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ty } - special_form::NonStdlibAlias::Top => { + special_form::MiscSpecialForm::Top => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1051,7 +1051,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; arg.top_materialization(db) } - special_form::NonStdlibAlias::Bottom => { + special_form::MiscSpecialForm::Bottom => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1075,7 +1075,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; arg.bottom_materialization(db) } - special_form::NonStdlibAlias::TypeOf => { + special_form::MiscSpecialForm::TypeOf => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1104,7 +1104,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { type_of_type } - special_form::NonStdlibAlias::CallableTypeOf => { + special_form::MiscSpecialForm::CallableTypeOf => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1154,21 +1154,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } callable_type } - special_form::NonStdlibAlias::ClassVar - | special_form::NonStdlibAlias::Final - | special_form::NonStdlibAlias::Required - | special_form::NonStdlibAlias::NotRequired - | special_form::NonStdlibAlias::ReadOnly => { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - let diag = builder.into_diagnostic(format_args!( - "Type qualifier `{special_form}` is not allowed in type expressions \ - (only in annotation expressions)", - )); - diagnostic::add_type_expression_reference_link(diag); - } - self.infer_type_expression(arguments_slice) - } - special_form::NonStdlibAlias::TypeIs => match arguments_slice { + special_form::MiscSpecialForm::TypeIs => match arguments_slice { ast::Expr::Tuple(_) => { self.infer_type_expression(arguments_slice); @@ -1193,11 +1179,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .top_materialization(self.db()), ), }, - special_form::NonStdlibAlias::TypeGuard => { + special_form::MiscSpecialForm::TypeGuard => { self.infer_type_expression(arguments_slice); todo_type!("`TypeGuard[]` special form") } - special_form::NonStdlibAlias::Concatenate => { + special_form::MiscSpecialForm::Concatenate => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1222,14 +1208,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } inferred_type } - special_form::NonStdlibAlias::Unpack => { + special_form::MiscSpecialForm::Unpack => { self.infer_type_expression(arguments_slice); todo_type!("`Unpack[]` special form") } - special_form::NonStdlibAlias::NoReturn - | special_form::NonStdlibAlias::Never - | special_form::NonStdlibAlias::AlwaysTruthy - | special_form::NonStdlibAlias::AlwaysFalsy => { + special_form::MiscSpecialForm::NoReturn + | special_form::MiscSpecialForm::Never + | special_form::MiscSpecialForm::AlwaysTruthy + | special_form::MiscSpecialForm::AlwaysFalsy => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1239,12 +1225,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - special_form::NonStdlibAlias::TypingSelf - | special_form::NonStdlibAlias::TypeAlias - | special_form::NonStdlibAlias::TypedDict - | special_form::NonStdlibAlias::Unknown - | special_form::NonStdlibAlias::Any - | special_form::NonStdlibAlias::NamedTuple => { + special_form::MiscSpecialForm::TypingSelf + | special_form::MiscSpecialForm::TypeAlias + | special_form::MiscSpecialForm::TypedDict + | special_form::MiscSpecialForm::Unknown + | special_form::MiscSpecialForm::Any + | special_form::MiscSpecialForm::NamedTuple => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1254,7 +1240,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - special_form::NonStdlibAlias::LiteralString => { + special_form::MiscSpecialForm::LiteralString => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1265,7 +1251,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - special_form::NonStdlibAlias::Generic | special_form::NonStdlibAlias::Protocol => { + special_form::MiscSpecialForm::Generic | special_form::MiscSpecialForm::Protocol => { self.infer_expression(arguments_slice, TypeContext::default()); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( @@ -1331,7 +1317,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { special_form::SpecialFormCategory::LegacyStdlibAlias(alias) => { self.infer_parameterized_legacy_typing_alias(subscript, alias) } - special_form::SpecialFormCategory::NonStdlibAlias(alias) => { + special_form::SpecialFormCategory::Other(alias) => { self.infer_parameterized_non_stdlib_alias_special_form(subscript, alias) } special_form::SpecialFormCategory::Tuple => { @@ -1343,6 +1329,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { special_form::SpecialFormCategory::Callable => { self.infer_callable_type_expression(subscript) } + special_form::SpecialFormCategory::TypeQualifier(qualifier) => { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let diag = builder.into_diagnostic(format_args!( + "Type qualifier `{qualifier}` is not allowed in type expressions \ + (only in annotation expressions)", + )); + diagnostic::add_type_expression_reference_link(diag); + } + self.infer_type_expression(&subscript.slice); + Type::unknown() + } } } diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 01de2d27818e7..6368b362ad8d7 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -5,6 +5,7 @@ use super::{ClassType, Type, class::KnownClass}; use crate::db::Db; use crate::module_resolver::{KnownModule, file_to_module}; use crate::semantic_index::{definition::Definition, scope::ScopeId, semantic_index}; +use crate::types::TypeQualifiers; use crate::types::{ DynamicType, IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, generics::typing_self, infer::nearest_enclosing_class, @@ -391,6 +392,13 @@ impl SpecialFormType { Self::Tuple => SpecialFormCategory::Tuple, Self::Type => SpecialFormCategory::Type, + // Type qualifiers + Self::Final => SpecialFormCategory::TypeQualifier(TypeQualifier::Final), + Self::ClassVar => SpecialFormCategory::TypeQualifier(TypeQualifier::ClassVar), + Self::ReadOnly => SpecialFormCategory::TypeQualifier(TypeQualifier::ReadOnly), + Self::Required => SpecialFormCategory::TypeQualifier(TypeQualifier::Required), + Self::NotRequired => SpecialFormCategory::TypeQualifier(TypeQualifier::NotRequired), + // Legacy standard library aliases Self::List => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::List), Self::Dict => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Dict), @@ -407,42 +415,40 @@ impl SpecialFormType { } // Non-standard-library aliases - Self::AlwaysFalsy => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::AlwaysFalsy), - Self::Unknown => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Unknown), - Self::Not => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Not), - Self::TypeOf => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypeOf), - Self::Top => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Top), - Self::Bottom => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Bottom), - Self::Annotated => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Annotated), - Self::Any => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Any), - Self::Literal => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Literal), - Self::Optional => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Optional), - Self::Union => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Union), - Self::NoReturn => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::NoReturn), - Self::Never => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Never), - Self::Final => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Final), - Self::ClassVar => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::ClassVar), - Self::Concatenate => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Concatenate), - Self::Unpack => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Unpack), - Self::Required => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Required), - Self::NotRequired => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::NotRequired), - Self::TypeAlias => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypeAlias), - Self::TypeGuard => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypeGuard), - Self::TypedDict => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypedDict), - Self::TypeIs => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypeIs), - Self::ReadOnly => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::ReadOnly), - Self::Protocol => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Protocol), - Self::Generic => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Generic), - Self::NamedTuple => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::NamedTuple), - Self::AlwaysTruthy => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::AlwaysTruthy), - Self::Intersection => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::Intersection), - Self::TypingSelf => SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::TypingSelf), - Self::LiteralString => { - SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::LiteralString) - } - Self::CallableTypeOf => { - SpecialFormCategory::NonStdlibAlias(NonStdlibAlias::CallableTypeOf) - } + Self::AlwaysFalsy => SpecialFormCategory::Other(MiscSpecialForm::AlwaysFalsy), + Self::Unknown => SpecialFormCategory::Other(MiscSpecialForm::Unknown), + Self::Not => SpecialFormCategory::Other(MiscSpecialForm::Not), + Self::TypeOf => SpecialFormCategory::Other(MiscSpecialForm::TypeOf), + Self::Top => SpecialFormCategory::Other(MiscSpecialForm::Top), + Self::Bottom => SpecialFormCategory::Other(MiscSpecialForm::Bottom), + Self::Annotated => SpecialFormCategory::Other(MiscSpecialForm::Annotated), + Self::Any => SpecialFormCategory::Other(MiscSpecialForm::Any), + Self::Literal => SpecialFormCategory::Other(MiscSpecialForm::Literal), + Self::Optional => SpecialFormCategory::Other(MiscSpecialForm::Optional), + Self::Union => SpecialFormCategory::Other(MiscSpecialForm::Union), + Self::NoReturn => SpecialFormCategory::Other(MiscSpecialForm::NoReturn), + Self::Never => SpecialFormCategory::Other(MiscSpecialForm::Never), + Self::Concatenate => SpecialFormCategory::Other(MiscSpecialForm::Concatenate), + Self::Unpack => SpecialFormCategory::Other(MiscSpecialForm::Unpack), + Self::TypeAlias => SpecialFormCategory::Other(MiscSpecialForm::TypeAlias), + Self::TypeGuard => SpecialFormCategory::Other(MiscSpecialForm::TypeGuard), + Self::TypedDict => SpecialFormCategory::Other(MiscSpecialForm::TypedDict), + Self::TypeIs => SpecialFormCategory::Other(MiscSpecialForm::TypeIs), + Self::Protocol => SpecialFormCategory::Other(MiscSpecialForm::Protocol), + Self::Generic => SpecialFormCategory::Other(MiscSpecialForm::Generic), + Self::NamedTuple => SpecialFormCategory::Other(MiscSpecialForm::NamedTuple), + Self::AlwaysTruthy => SpecialFormCategory::Other(MiscSpecialForm::AlwaysTruthy), + Self::Intersection => SpecialFormCategory::Other(MiscSpecialForm::Intersection), + Self::TypingSelf => SpecialFormCategory::Other(MiscSpecialForm::TypingSelf), + Self::LiteralString => SpecialFormCategory::Other(MiscSpecialForm::LiteralString), + Self::CallableTypeOf => SpecialFormCategory::Other(MiscSpecialForm::CallableTypeOf), + } + } + + pub(super) const fn as_type_qualifier(self) -> Option { + match self.kind() { + SpecialFormCategory::TypeQualifier(qualifier) => Some(qualifier), + _ => None, } } } @@ -459,8 +465,8 @@ pub(super) enum SpecialFormCategory { /// Special forms that are simple aliases to classes elsewhere in the standard library. LegacyStdlibAlias(LegacyStdlibAlias), - /// Special forms that are not aliases to classes elsewhere in the standard library. - NonStdlibAlias(NonStdlibAlias), + /// Special forms that are type qualifiers + TypeQualifier(TypeQualifier), /// The special form `typing.Tuple`. /// @@ -481,6 +487,9 @@ pub(super) enum SpecialFormCategory { /// special handling for both type-expression parsing and `isinstance`/`issubclass` /// narrowing. Callable, + + /// Everything else... + Other(MiscSpecialForm), } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -543,6 +552,45 @@ impl std::fmt::Display for LegacyStdlibAlias { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum TypeQualifier { + ReadOnly, + Final, + ClassVar, + Required, + NotRequired, +} + +impl From for SpecialFormType { + fn from(value: TypeQualifier) -> Self { + match value { + TypeQualifier::ReadOnly => SpecialFormType::ReadOnly, + TypeQualifier::Final => SpecialFormType::Final, + TypeQualifier::ClassVar => SpecialFormType::ClassVar, + TypeQualifier::Required => SpecialFormType::Required, + TypeQualifier::NotRequired => SpecialFormType::NotRequired, + } + } +} + +impl From for TypeQualifiers { + fn from(value: TypeQualifier) -> Self { + match value { + TypeQualifier::ReadOnly => TypeQualifiers::READ_ONLY, + TypeQualifier::Final => TypeQualifiers::FINAL, + TypeQualifier::ClassVar => TypeQualifiers::CLASS_VAR, + TypeQualifier::Required => TypeQualifiers::REQUIRED, + TypeQualifier::NotRequired => TypeQualifiers::NOT_REQUIRED, + } + } +} + +impl std::fmt::Display for TypeQualifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + SpecialFormType::from(*self).fmt(f) + } +} + /// Information regarding the [`KnownClass`] a [`LegacyStdlibAlias`] refers to. pub(super) struct AliasSpec { pub(super) class: KnownClass, @@ -552,7 +600,7 @@ pub(super) struct AliasSpec { /// Enumeration of special forms that are not aliases to classes or special constructs /// elsewhere in the Python standard library. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) enum NonStdlibAlias { +pub(super) enum MiscSpecialForm { Any, Annotated, Literal, @@ -571,23 +619,18 @@ pub(super) enum NonStdlibAlias { Top, Bottom, TypingSelf, - Final, - ClassVar, Concatenate, Unpack, - Required, - NotRequired, TypeAlias, TypeGuard, TypedDict, TypeIs, - ReadOnly, Protocol, Generic, NamedTuple, } -impl NonStdlibAlias { +impl MiscSpecialForm { pub(super) fn in_type_expression<'db>( self, db: &'db dyn Db, @@ -674,72 +717,51 @@ impl NonStdlibAlias { ], fallback_type: Type::unknown(), }), - - Self::ClassVar | Self::Final => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::TypeQualifier(self) - ], - fallback_type: Type::unknown(), - }), - - Self::ReadOnly | Self::NotRequired | Self::Required => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::TypeQualifierRequiresOneArgument(self) - ], - fallback_type: Type::unknown(), - }) - } } } } -impl From for SpecialFormType { - fn from(value: NonStdlibAlias) -> Self { +impl From for SpecialFormType { + fn from(value: MiscSpecialForm) -> Self { match value { - NonStdlibAlias::Any => SpecialFormType::Any, - NonStdlibAlias::Annotated => SpecialFormType::Annotated, - NonStdlibAlias::Literal => SpecialFormType::Literal, - NonStdlibAlias::LiteralString => SpecialFormType::LiteralString, - NonStdlibAlias::Optional => SpecialFormType::Optional, - NonStdlibAlias::Union => SpecialFormType::Union, - NonStdlibAlias::NoReturn => SpecialFormType::NoReturn, - NonStdlibAlias::Never => SpecialFormType::Never, - NonStdlibAlias::Unknown => SpecialFormType::Unknown, - NonStdlibAlias::AlwaysTruthy => SpecialFormType::AlwaysTruthy, - NonStdlibAlias::AlwaysFalsy => SpecialFormType::AlwaysFalsy, - NonStdlibAlias::Not => SpecialFormType::Not, - NonStdlibAlias::Intersection => SpecialFormType::Intersection, - NonStdlibAlias::TypeOf => SpecialFormType::TypeOf, - NonStdlibAlias::CallableTypeOf => SpecialFormType::CallableTypeOf, - NonStdlibAlias::Top => SpecialFormType::Top, - NonStdlibAlias::Bottom => SpecialFormType::Bottom, - NonStdlibAlias::TypingSelf => SpecialFormType::TypingSelf, - NonStdlibAlias::Final => SpecialFormType::Final, - NonStdlibAlias::ClassVar => SpecialFormType::ClassVar, - NonStdlibAlias::Concatenate => SpecialFormType::Concatenate, - NonStdlibAlias::Unpack => SpecialFormType::Unpack, - NonStdlibAlias::Required => SpecialFormType::Required, - NonStdlibAlias::NotRequired => SpecialFormType::NotRequired, - NonStdlibAlias::TypeAlias => SpecialFormType::TypeAlias, - NonStdlibAlias::TypeGuard => SpecialFormType::TypeGuard, - NonStdlibAlias::TypedDict => SpecialFormType::TypedDict, - NonStdlibAlias::TypeIs => SpecialFormType::TypeIs, - NonStdlibAlias::ReadOnly => SpecialFormType::ReadOnly, - NonStdlibAlias::Protocol => SpecialFormType::Protocol, - NonStdlibAlias::Generic => SpecialFormType::Generic, - NonStdlibAlias::NamedTuple => SpecialFormType::NamedTuple, + MiscSpecialForm::Any => SpecialFormType::Any, + MiscSpecialForm::Annotated => SpecialFormType::Annotated, + MiscSpecialForm::Literal => SpecialFormType::Literal, + MiscSpecialForm::LiteralString => SpecialFormType::LiteralString, + MiscSpecialForm::Optional => SpecialFormType::Optional, + MiscSpecialForm::Union => SpecialFormType::Union, + MiscSpecialForm::NoReturn => SpecialFormType::NoReturn, + MiscSpecialForm::Never => SpecialFormType::Never, + MiscSpecialForm::Unknown => SpecialFormType::Unknown, + MiscSpecialForm::AlwaysTruthy => SpecialFormType::AlwaysTruthy, + MiscSpecialForm::AlwaysFalsy => SpecialFormType::AlwaysFalsy, + MiscSpecialForm::Not => SpecialFormType::Not, + MiscSpecialForm::Intersection => SpecialFormType::Intersection, + MiscSpecialForm::TypeOf => SpecialFormType::TypeOf, + MiscSpecialForm::CallableTypeOf => SpecialFormType::CallableTypeOf, + MiscSpecialForm::Top => SpecialFormType::Top, + MiscSpecialForm::Bottom => SpecialFormType::Bottom, + MiscSpecialForm::TypingSelf => SpecialFormType::TypingSelf, + MiscSpecialForm::Concatenate => SpecialFormType::Concatenate, + MiscSpecialForm::Unpack => SpecialFormType::Unpack, + MiscSpecialForm::TypeAlias => SpecialFormType::TypeAlias, + MiscSpecialForm::TypeGuard => SpecialFormType::TypeGuard, + MiscSpecialForm::TypedDict => SpecialFormType::TypedDict, + MiscSpecialForm::TypeIs => SpecialFormType::TypeIs, + MiscSpecialForm::Protocol => SpecialFormType::Protocol, + MiscSpecialForm::Generic => SpecialFormType::Generic, + MiscSpecialForm::NamedTuple => SpecialFormType::NamedTuple, } } } -impl From for Type<'_> { - fn from(value: NonStdlibAlias) -> Self { +impl From for Type<'_> { + fn from(value: MiscSpecialForm) -> Self { Type::SpecialForm(SpecialFormType::from(value)) } } -impl std::fmt::Display for NonStdlibAlias { +impl std::fmt::Display for MiscSpecialForm { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { SpecialFormType::from(*self).fmt(f) } From a23f7e2ac91048a9018492d59fdc84579d59170f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 22:38:59 +0000 Subject: [PATCH 06/11] Simplify SpecialForm code using SpecialFormCategory Refactor match arms on SpecialFormType variants to use exhaustive matches over SpecialFormCategory where possible: - In annotation_expression.rs, replace .as_type_qualifier().expect(...) with inner exhaustive matches on special_form.kind(), eliminating two .expect() calls and merging separate SpecialForm match arms. - Convert is_valid_in_type_expression and is_valid_isinstance_target from matches!/!matches! to exhaustive match expressions, so the compiler enforces updates when new SpecialFormCategory variants are added. - Remove the now-unused as_type_qualifier method. https://claude.ai/code/session_01HXMzTUkfPVJCxdxTzZdePf --- .../resources/mdtest/implicit_type_aliases.md | 8 +- crates/ty_python_semantic/src/types.rs | 11 +- .../src/types/class_base.rs | 2 +- .../src/types/infer/builder.rs | 442 ++++++++---------- .../infer/builder/annotation_expression.rs | 296 ++++++------ .../types/infer/builder/type_expression.rs | 6 +- .../src/types/special_form.rs | 131 ++---- 7 files changed, 392 insertions(+), 504 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 292b15ec32ace..66fc452d8ac79 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -1443,7 +1443,7 @@ from typing import List, Dict # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" InvalidList = List[1] -# error: [invalid-type-form] "`typing.List` requires exactly one argument" +# error: [invalid-type-form] "`typing.List` requires exactly 1 argument, got 2" ListTooManyArgs = List[int, str] # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" @@ -1452,10 +1452,10 @@ InvalidDict1 = Dict[1, str] # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" InvalidDict2 = Dict[str, 2] -# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 1" +# error: [invalid-type-form] "`typing.Dict` requires exactly 2 arguments, got 1" DictTooFewArgs = Dict[str] -# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 3" +# error: [invalid-type-form] "`typing.Dict` requires exactly 2 arguments, got 3" DictTooManyArgs = Dict[str, int, float] def _( @@ -1470,7 +1470,7 @@ def _( reveal_type(list_too_many_args) # revealed: list[Unknown] reveal_type(invalid_dict1) # revealed: dict[Unknown, str] reveal_type(invalid_dict2) # revealed: dict[str, Unknown] - reveal_type(dict_too_few_args) # revealed: dict[str, Unknown] + reveal_type(dict_too_few_args) # revealed: dict[Unknown, Unknown] reveal_type(dict_too_many_args) # revealed: dict[Unknown, Unknown] ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6365c9080b410..e737d36469b83 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5998,8 +5998,8 @@ impl<'db> Type<'db> { SpecialFormCategory::LegacyStdlibAlias(alias) => { Ok(alias.aliased_class().to_instance(db)) } - SpecialFormCategory::Other(alias) => { - alias.in_type_expression(db, scope_id, typevar_binding_context) + SpecialFormCategory::Other(form) => { + form.in_type_expression(db, scope_id, typevar_binding_context) } SpecialFormCategory::TypeQualifier(qualifier) => { let err = match qualifier { @@ -7826,7 +7826,7 @@ impl TypeQualifiers { pub(crate) struct TypeAndQualifiers<'db> { inner: Type<'db>, origin: TypeOrigin, - pub(crate) qualifiers: TypeQualifiers, + qualifiers: TypeQualifiers, } impl<'db> TypeAndQualifiers<'db> { @@ -7855,9 +7855,10 @@ impl<'db> TypeAndQualifiers<'db> { self.origin } - /// Insert/add an additional type qualifier in place. - pub(crate) fn add_qualifier(&mut self, qualifier: TypeQualifiers) { + /// Return `self` with an additional qualifier added to the set of qualifiers. + pub(crate) fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { self.qualifiers |= qualifier; + self } /// Return the set of type qualifiers. diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 8fa8fc157dadc..55fa527b98045 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -2,13 +2,13 @@ use crate::types::class::CodeGeneratorKind; use crate::types::generics::{ApplySpecialization, Specialization}; use crate::types::mro::MroIterator; use crate::types::special_form::{self, SpecialFormCategory}; -use crate::{Db, DisplaySettings}; use crate::types::tuple::TupleType; use crate::types::{ ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, SpecialFormType, StaticMroError, Type, TypeContext, TypeMapping, todo_type, }; +use crate::{Db, DisplaySettings}; /// Enumeration of the possible kinds of types we allow in class bases. /// diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f05127b4dc1f4..b72c4690af96e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -119,6 +119,7 @@ use crate::types::generics::{ use crate::types::infer::nearest_enclosing_function; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; +use crate::types::special_form::{AliasSpec, MiscSpecialForm}; use crate::types::subclass_of::SubclassOfInner; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleSpecBuilder, TupleType}; @@ -132,12 +133,13 @@ use crate::types::{ IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, - Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, - TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, TypeVarDefaultEvaluation, - TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, - UnionType, UnionTypeInstance, any_over_type, binding_type, definition_expression_type, - infer_complete_scope_types, infer_scope_types, todo_type, + Parameters, Signature, SpecialFormCategory, SpecialFormType, StaticClassLiteral, + SubclassOfType, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, + TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, + TypeVarConstraints, TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, + TypeVarVariance, TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, any_over_type, + binding_type, definition_expression_type, infer_complete_scope_types, infer_scope_types, + todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; @@ -15933,279 +15935,243 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } - Type::SpecialForm(SpecialFormType::Tuple) => { - return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(subscript)); - } - Type::SpecialForm(SpecialFormType::Literal) => { - match self.infer_literal_parameter_type(slice) { - Ok(result) => { - return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( - self.db(), - result, - ))); - } - Err(nodes) => { - for node in nodes { - let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) - else { - continue; - }; - builder.into_diagnostic( - "Type arguments for `Literal` must be `None`, \ + Type::SpecialForm(special_form) => match special_form.kind() { + SpecialFormCategory::Tuple => { + return tuple_generic_alias( + self.db(), + self.infer_tuple_type_expression(subscript), + ); + } + SpecialFormCategory::Other(MiscSpecialForm::Literal) => { + match self.infer_literal_parameter_type(slice) { + Ok(result) => { + return Type::KnownInstance(KnownInstanceType::Literal( + InternedType::new(self.db(), result), + )); + } + Err(nodes) => { + for node in nodes { + let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, node) + else { + continue; + }; + builder.into_diagnostic( + "Type arguments for `Literal` must be `None`, \ a literal value (int, bool, str, or bytes), or an enum member", - ); + ); + } + return Type::unknown(); } - return Type::unknown(); } } - } - Type::SpecialForm(SpecialFormType::Annotated) => { - let ast::Expr::Tuple(ast::ExprTuple { - elts: ref arguments, - .. - }) = **slice - else { - report_invalid_arguments_to_annotated(&self.context, subscript); - - return self.infer_expression(slice, TypeContext::default()); - }; + SpecialFormCategory::Other(MiscSpecialForm::Annotated) => { + let ast::Expr::Tuple(ast::ExprTuple { + elts: ref arguments, + .. + }) = **slice + else { + report_invalid_arguments_to_annotated(&self.context, subscript); - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } + return self.infer_expression(slice, TypeContext::default()); + }; - let [type_expr, metadata @ ..] = &arguments[..] else { - for argument in arguments { - self.infer_expression(argument, TypeContext::default()); + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); } - self.store_expression_type(slice, Type::unknown()); - return Type::unknown(); - }; - for element in metadata { - self.infer_expression(element, TypeContext::default()); - } + let [type_expr, metadata @ ..] = &arguments[..] else { + for argument in arguments { + self.infer_expression(argument, TypeContext::default()); + } + self.store_expression_type(slice, Type::unknown()); + return Type::unknown(); + }; - let ty = self.infer_type_expression(type_expr); + for element in metadata { + self.infer_expression(element, TypeContext::default()); + } - return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( - self.db(), - ty, - ))); - } - Type::SpecialForm(SpecialFormType::Optional) => { - let db = self.db(); + let ty = self.infer_type_expression(type_expr); - if matches!(**slice, ast::Expr::Tuple(_)) - && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "`typing.Optional` requires exactly one argument" - )); + return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( + self.db(), + ty, + ))); } + SpecialFormCategory::Other(MiscSpecialForm::Optional) => { + let db = self.db(); + + if matches!(**slice, ast::Expr::Tuple(_)) + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "`typing.Optional` requires exactly one argument" + )); + } - let ty = self.infer_type_expression(slice); + let ty = self.infer_type_expression(slice); - // `Optional[None]` is equivalent to `None`: - if ty.is_none(db) { - return ty; + // `Optional[None]` is equivalent to `None`: + if ty.is_none(db) { + return ty; + } + + return Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + None, + Ok(UnionType::from_elements(db, [ty, Type::none(db)])), + ), + )); } + SpecialFormCategory::Other(MiscSpecialForm::Union) => { + let db = self.db(); - return Type::KnownInstance(KnownInstanceType::UnionType(UnionTypeInstance::new( - db, - None, - Ok(UnionType::from_elements(db, [ty, Type::none(db)])), - ))); - } - Type::SpecialForm(SpecialFormType::Union) => { - let db = self.db(); + match **slice { + ast::Expr::Tuple(ref tuple) => { + let mut elements = tuple + .elts + .iter() + .map(|elt| self.infer_type_expression(elt)) + .peekable(); - match **slice { - ast::Expr::Tuple(ref tuple) => { - let mut elements = tuple - .elts - .iter() - .map(|elt| self.infer_type_expression(elt)) - .peekable(); + let is_empty = elements.peek().is_none(); + let union_type = Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + None, + Ok(UnionType::from_elements(db, elements)), + ), + )); - let is_empty = elements.peek().is_none(); - let union_type = Type::KnownInstance(KnownInstanceType::UnionType( - UnionTypeInstance::new( - db, - None, - Ok(UnionType::from_elements(db, elements)), - ), - )); + if is_empty + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic( + "`typing.Union` requires at least one type argument", + ); + } - if is_empty - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic( - "`typing.Union` requires at least one type argument", - ); + return union_type; + } + _ => { + return self.infer_expression(slice, TypeContext::default()); } - - return union_type; - } - _ => { - return self.infer_expression(slice, TypeContext::default()); } } - } - Type::SpecialForm(SpecialFormType::Type) => { - // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` - let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( - InternedType::new(self.db(), argument_ty), - )); - } - Type::SpecialForm(SpecialFormType::Callable) => { - let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { - &*tuple.elts - } else { - std::slice::from_ref(&*subscript.slice) - }; + SpecialFormCategory::Type => { + // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` + let argument_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + InternedType::new(self.db(), argument_ty), + )); + } + SpecialFormCategory::Callable => { + let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { + &*tuple.elts + } else { + std::slice::from_ref(&*subscript.slice) + }; - // TODO: Remove this once we support Concatenate properly. This is necessary - // to avoid a lot of false positives downstream, because we can't represent the typevar- - // specialized `Callable` types yet. - let num_arguments = arguments.len(); - if num_arguments == 2 { - let first_arg = &arguments[0]; - let second_arg = &arguments[1]; - - if first_arg.is_subscript_expr() { - let first_arg_ty = self.infer_expression(first_arg, TypeContext::default()); - if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) = - first_arg_ty - { - let mut variables = generic_context - .variables(self.db()) - .collect::>(); + // TODO: Remove this once we support Concatenate properly. This is necessary + // to avoid a lot of false positives downstream, because we can't represent the typevar- + // specialized `Callable` types yet. + let num_arguments = arguments.len(); + if num_arguments == 2 { + let first_arg = &arguments[0]; + let second_arg = &arguments[1]; + + if first_arg.is_subscript_expr() { + let first_arg_ty = + self.infer_expression(first_arg, TypeContext::default()); + if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) = + first_arg_ty + { + let mut variables = generic_context + .variables(self.db()) + .collect::>(); - let return_ty = - self.infer_expression(second_arg, TypeContext::default()); - return_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); + let return_ty = + self.infer_expression(second_arg, TypeContext::default()); + return_ty.bind_and_find_all_legacy_typevars( + self.db(), + self.typevar_binding_context, + &mut variables, + ); - let generic_context = - GenericContext::from_typevar_instances(self.db(), variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } + let generic_context = + GenericContext::from_typevar_instances(self.db(), variables); + return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); + } - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( "The first argument to `Callable` must be either a list of types, \ ParamSpec, Concatenate, or `...`", )); + } + return Type::KnownInstance(KnownInstanceType::Callable( + CallableType::unknown(self.db()), + )); } - return Type::KnownInstance(KnownInstanceType::Callable( - CallableType::unknown(self.db()), - )); } - } - let callable = self - .infer_callable_type(subscript) - .as_callable() - .expect("always returns Type::Callable"); + let callable = self + .infer_callable_type(subscript) + .as_callable() + .expect("always returns Type::Callable"); - return Type::KnownInstance(KnownInstanceType::Callable(callable)); - } - // `typing` special forms with a single generic argument - Type::SpecialForm( - special_form @ (SpecialFormType::List - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::Counter - | SpecialFormType::Deque), - ) => { - let slice_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::Callable(callable)); + } + SpecialFormCategory::Other(_) => {} + SpecialFormCategory::LegacyStdlibAlias(alias) => { + let AliasSpec { + class, + expected_argument_number, + } = alias.alias_spec(); - let element_ty = if matches!(**slice, ast::Expr::Tuple(_)) { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly one argument", - special_form.name() - )); - } - Type::unknown() - } else { - slice_ty - }; + let args = if let ast::Expr::Tuple(t) = &**slice { + &*t.elts + } else { + std::slice::from_ref(&**slice) + }; - let class = special_form - .aliased_stdlib_class() - .expect("A known stdlib class is available"); - - return class - .to_specialized_class_type(self.db(), &[element_ty]) - .map(Type::from) - .unwrap_or_else(Type::unknown); - } - // `typing` special forms with two generic arguments - Type::SpecialForm( - special_form @ (SpecialFormType::Dict - | SpecialFormType::ChainMap - | SpecialFormType::DefaultDict - | SpecialFormType::OrderedDict), - ) => { - let (first_ty, second_ty) = if let ast::Expr::Tuple(ast::ExprTuple { - elts: ref arguments, - .. - }) = **slice - { - if arguments.len() != 2 - && let Some(builder) = + if args.len() != expected_argument_number { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly two arguments, got {}", - special_form.name(), - arguments.len() - )); - } - - if let [first_expr, second_expr] = &arguments[..] { - let first_ty = self.infer_type_expression(first_expr); - let second_ty = self.infer_type_expression(second_expr); - - (first_ty, second_ty) - } else { - for argument in arguments { - self.infer_type_expression(argument); + { + let noun = if expected_argument_number == 1 { + "argument" + } else { + "arguments" + }; + builder.into_diagnostic(format_args!( + "`typing.{name}` requires exactly \ + {expected_argument_number} {noun}, got {got}", + name = special_form.name(), + got = args.len() + )); } - - (Type::unknown(), Type::unknown()) - } - } else { - let first_ty = self.infer_type_expression(slice); - - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly two arguments, got 1", - special_form.name() - )); } - (first_ty, Type::unknown()) - }; + let arg_types: Vec<_> = args + .iter() + .map(|arg| self.infer_type_expression(arg)) + .collect(); - let class = special_form - .aliased_stdlib_class() - .expect("Stdlib class available"); + return class + .to_specialized_class_type(self.db(), arg_types) + .map(Type::from) + .unwrap_or_else(Type::unknown); + } + SpecialFormCategory::TypeQualifier(_) => {} + }, - return class - .to_specialized_class_type(self.db(), &[first_ty, second_ty]) - .map(Type::from) - .unwrap_or_else(Type::unknown); - } Type::KnownInstance( KnownInstanceType::UnionType(_) | KnownInstanceType::Annotated(_) diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index e57e43f056409..fe521ba3860f1 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -6,11 +6,13 @@ use crate::types::diagnostic::{ INVALID_TYPE_FORM, REDUNDANT_FINAL_CLASSVAR, report_invalid_arguments_to_annotated, }; use crate::types::infer::nearest_enclosing_class; +use crate::types::special_form::{MiscSpecialForm, SpecialFormCategory}; use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, }; use crate::types::{ - KnownClass, SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifiers, todo_type, + KnownClass, SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifier, + TypeQualifiers, todo_type, }; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -90,36 +92,37 @@ impl<'db> TypeInferenceBuilder<'db, '_> { pep_613_policy: PEP613Policy, ) -> TypeAndQualifiers<'db> { match ty { - Type::SpecialForm(SpecialFormType::ClassVar) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::CLASS_VAR, - ), - Type::SpecialForm(SpecialFormType::Final) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::FINAL, - ), - Type::SpecialForm(SpecialFormType::Required) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::REQUIRED, - ), - Type::SpecialForm(SpecialFormType::NotRequired) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::NOT_REQUIRED, - ), - Type::SpecialForm(SpecialFormType::ReadOnly) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::READ_ONLY, - ), - Type::SpecialForm(SpecialFormType::TypeAlias) - if pep_613_policy == PEP613Policy::Allowed => - { - TypeAndQualifiers::declared(ty) - } + Type::SpecialForm(special_form) => match special_form.kind() { + SpecialFormCategory::TypeQualifier(qualifier) => TypeAndQualifiers::new( + Type::unknown(), + TypeOrigin::Declared, + TypeQualifiers::from(qualifier), + ), + SpecialFormCategory::Other(MiscSpecialForm::TypeAlias) + if pep_613_policy == PEP613Policy::Allowed => + { + TypeAndQualifiers::declared(ty) + } + SpecialFormCategory::Type + | SpecialFormCategory::Tuple + | SpecialFormCategory::Callable + | SpecialFormCategory::LegacyStdlibAlias(_) + | SpecialFormCategory::Other(_) => TypeAndQualifiers::declared( + ty.default_specialize(builder.db()) + .in_type_expression( + builder.db(), + builder.scope(), + builder.typevar_binding_context, + ) + .unwrap_or_else(|error| { + error.into_fallback_type( + &builder.context, + annotation, + builder.is_reachable(annotation), + ) + }), + ), + }, // Conditional import of `typing.TypeAlias` or `typing_extensions.TypeAlias` on a // Python version where the former doesn't exist. Type::Union(union) @@ -228,135 +231,133 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let slice = &**slice; match value_ty { - Type::SpecialForm(SpecialFormType::Annotated) => { - // This branch is similar to the corresponding branch in `infer_parameterized_special_form_type_expression`, but - // `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly - // differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`). - if let ast::Expr::Tuple(ast::ExprTuple { - elts: arguments, .. - }) = slice - { - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } - - if let [inner_annotation, metadata @ ..] = &arguments[..] { - for element in metadata { - self.infer_expression(element, TypeContext::default()); + Type::SpecialForm(special_form) => match special_form.kind() { + SpecialFormCategory::Other(MiscSpecialForm::Annotated) => { + // This branch is similar to the corresponding branch in + // `infer_parameterized_special_form_type_expression`, but + // `Annotated[…]` can appear both in annotation expressions and in + // type expressions, and needs to be handled slightly + // differently in each case (calling either `infer_type_expression_*` + // or `infer_annotation_expression_*`). + if let ast::Expr::Tuple(ast::ExprTuple { + elts: arguments, .. + }) = slice + { + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); } - let inner_annotation_ty = self.infer_annotation_expression_impl( - inner_annotation, - PEP613Policy::Disallowed, - ); + if let [inner_annotation, metadata @ ..] = &arguments[..] { + for element in metadata { + self.infer_expression(element, TypeContext::default()); + } - self.store_expression_type(slice, inner_annotation_ty.inner_type()); - inner_annotation_ty - } else { - for argument in arguments { - self.infer_expression(argument, TypeContext::default()); + let inner_annotation_ty = self + .infer_annotation_expression_impl( + inner_annotation, + PEP613Policy::Disallowed, + ); + + self.store_expression_type( + slice, + inner_annotation_ty.inner_type(), + ); + inner_annotation_ty + } else { + for argument in arguments { + self.infer_expression(argument, TypeContext::default()); + } + self.store_expression_type(slice, Type::unknown()); + TypeAndQualifiers::declared(Type::unknown()) } - self.store_expression_type(slice, Type::unknown()); - TypeAndQualifiers::declared(Type::unknown()) + } else { + report_invalid_arguments_to_annotated(&self.context, subscript); + self.infer_annotation_expression_impl( + slice, + PEP613Policy::Disallowed, + ) } - } else { - report_invalid_arguments_to_annotated(&self.context, subscript); - self.infer_annotation_expression_impl(slice, PEP613Policy::Disallowed) } - } - Type::SpecialForm( - type_qualifier @ (SpecialFormType::ClassVar - | SpecialFormType::Final - | SpecialFormType::Required - | SpecialFormType::NotRequired - | SpecialFormType::ReadOnly), - ) => { - let arguments = if let ast::Expr::Tuple(tuple) = slice { - &*tuple.elts - } else { - std::slice::from_ref(slice) - }; - let type_and_qualifiers = if let [argument] = arguments { - let mut type_and_qualifiers = self.infer_annotation_expression_impl( - argument, - PEP613Policy::Disallowed, - ); - - // Emit a diagnostic if ClassVar and Final are combined in a class that is - // not a dataclass, since Final already implies the semantics of ClassVar. - let classvar_and_final = match type_qualifier { - SpecialFormType::Final => type_and_qualifiers - .qualifiers - .contains(TypeQualifiers::CLASS_VAR), - SpecialFormType::ClassVar => type_and_qualifiers - .qualifiers - .contains(TypeQualifiers::FINAL), - _ => false, + SpecialFormCategory::TypeQualifier(qualifier) => { + let arguments = if let ast::Expr::Tuple(tuple) = slice { + &*tuple.elts + } else { + std::slice::from_ref(slice) }; - if classvar_and_final - && nearest_enclosing_class(self.db(), self.index, self.scope()) - .is_none_or(|class| !class.is_dataclass_like(self.db())) - && let Some(builder) = self - .context - .report_lint(&REDUNDANT_FINAL_CLASSVAR, subscript) - { - builder.into_diagnostic(format_args!( - "`Combining `ClassVar` and `Final` is redundant" - )); - } + let type_and_qualifiers = if let [argument] = arguments { + let type_and_qualifiers = self.infer_annotation_expression_impl( + argument, + PEP613Policy::Disallowed, + ); + + // Emit a diagnostic if ClassVar and Final are combined in a class that is + // not a dataclass, since Final already implies the semantics of ClassVar. + let classvar_and_final = match qualifier { + TypeQualifier::Final => type_and_qualifiers + .qualifiers + .contains(TypeQualifiers::CLASS_VAR), + TypeQualifier::ClassVar => type_and_qualifiers + .qualifiers + .contains(TypeQualifiers::FINAL), + _ => false, + }; + if classvar_and_final + && nearest_enclosing_class(self.db(), self.index, self.scope()) + .is_none_or(|class| !class.is_dataclass_like(self.db())) + && let Some(builder) = self + .context + .report_lint(&REDUNDANT_FINAL_CLASSVAR, subscript) + { + builder.into_diagnostic(format_args!( + "`Combining `ClassVar` and `Final` is redundant" + )); + } - match type_qualifier { - SpecialFormType::ClassVar => { - type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR); - if type_and_qualifiers + if qualifier == TypeQualifier::ClassVar + && type_and_qualifiers .inner_type() .has_non_self_typevar(self.db()) - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic( - "`ClassVar` cannot contain type variables", - ); - } - } - SpecialFormType::Final => { - type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL); + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic( + "`ClassVar` cannot contain type variables", + ); } - SpecialFormType::Required => { - type_and_qualifiers.add_qualifier(TypeQualifiers::REQUIRED); - } - SpecialFormType::NotRequired => { - type_and_qualifiers.add_qualifier(TypeQualifiers::NOT_REQUIRED); + type_and_qualifiers.with_qualifier(TypeQualifiers::from(qualifier)) + } else { + for element in arguments { + self.infer_annotation_expression_impl( + element, + PEP613Policy::Disallowed, + ); } - SpecialFormType::ReadOnly => { - type_and_qualifiers.add_qualifier(TypeQualifiers::READ_ONLY); + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + let num_arguments = arguments.len(); + builder.into_diagnostic(format_args!( + "Type qualifier `{qualifier}` expected exactly 1 \ + argument, got {num_arguments}", + )); } - _ => unreachable!(), + TypeAndQualifiers::declared(Type::unknown()) + }; + if slice.is_tuple_expr() { + self.store_expression_type(slice, type_and_qualifiers.inner_type()); } type_and_qualifiers - } else { - for element in arguments { - self.infer_annotation_expression_impl( - element, - PEP613Policy::Disallowed, - ); - } - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - let num_arguments = arguments.len(); - builder.into_diagnostic(format_args!( - "Type qualifier `{type_qualifier}` expected exactly 1 argument, \ - got {num_arguments}", - )); - } - TypeAndQualifiers::declared(Type::unknown()) - }; - if slice.is_tuple_expr() { - self.store_expression_type(slice, type_and_qualifiers.inner_type()); } - type_and_qualifiers - } + SpecialFormCategory::Type + | SpecialFormCategory::Tuple + | SpecialFormCategory::Callable + | SpecialFormCategory::LegacyStdlibAlias(_) + | SpecialFormCategory::Other(_) => TypeAndQualifiers::declared( + self.infer_subscript_type_expression_no_store( + subscript, slice, value_ty, + ), + ), + }, Type::ClassLiteral(class) if class.is_known(self.db(), KnownClass::InitVar) => { let arguments = if let ast::Expr::Tuple(tuple) = slice { &*tuple.elts @@ -364,12 +365,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { std::slice::from_ref(slice) }; let type_and_qualifiers = if let [argument] = arguments { - let mut type_and_qualifiers = self.infer_annotation_expression_impl( + self.infer_annotation_expression_impl( argument, PEP613Policy::Disallowed, - ); - type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR); - type_and_qualifiers + ) + .with_qualifier(TypeQualifiers::INIT_VAR) } else { for element in arguments { self.infer_annotation_expression_impl( diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 802f2e2fa92a6..ac31be26425c3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1700,10 +1700,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - fn infer_callable_type_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { - self.infer_callable_type(subscript) - } - fn infer_parameterized_special_form_type_expression( &mut self, subscript: &ast::ExprSubscript, @@ -1723,7 +1719,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_subclass_of_type_expression(&subscript.slice) } special_form::SpecialFormCategory::Callable => { - self.infer_callable_type_expression(subscript) + self.infer_callable_type(subscript) } special_form::SpecialFormCategory::TypeQualifier(qualifier) => { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 23962a0dee271..6709eb3df9fad 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -4,12 +4,14 @@ use super::{ClassType, Type, class::KnownClass}; use crate::db::Db; use crate::semantic_index::place::ScopedPlaceId; -use crate::semantic_index::{FileScopeId, definition::Definition, place_table, scope::ScopeId, semantic_index, use_def_map}; +use crate::semantic_index::{ + FileScopeId, definition::Definition, place_table, scope::ScopeId, semantic_index, use_def_map, +}; use crate::types::TypeDefinition; use crate::types::TypeQualifiers; use crate::types::{ - IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, - generics::typing_self, infer::nearest_enclosing_class, + IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, generics::typing_self, + infer::nearest_enclosing_class, }; use ruff_db::files::File; use std::str::FromStr; @@ -341,51 +343,16 @@ impl SpecialFormType { /// specification for more details: /// pub(super) const fn is_valid_in_type_expression(self) -> bool { - match self { - Self::ClassVar - | Self::Final - | Self::Required - | Self::NotRequired - | SpecialFormType::ReadOnly - | SpecialFormType::Unpack - | SpecialFormType::TypeAlias => false, - Self::Annotated - | SpecialFormType::Any - | SpecialFormType::Literal - | SpecialFormType::LiteralString - | SpecialFormType::Optional - | SpecialFormType::Union - | SpecialFormType::NoReturn - | SpecialFormType::Never - | SpecialFormType::Tuple - | SpecialFormType::List - | SpecialFormType::Dict - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::ChainMap - | SpecialFormType::Counter - | SpecialFormType::DefaultDict - | SpecialFormType::Deque - | SpecialFormType::OrderedDict - | SpecialFormType::Type - | SpecialFormType::Unknown - | SpecialFormType::AlwaysTruthy - | SpecialFormType::AlwaysFalsy - | SpecialFormType::Not - | SpecialFormType::Intersection - | SpecialFormType::TypeOf - | SpecialFormType::CallableTypeOf - | SpecialFormType::Top - | SpecialFormType::Bottom - | SpecialFormType::Callable - | SpecialFormType::TypingSelf - | SpecialFormType::Concatenate - | SpecialFormType::TypeGuard - | SpecialFormType::TypedDict - | SpecialFormType::TypeIs - | SpecialFormType::Protocol - | SpecialFormType::Generic - | SpecialFormType::NamedTuple => true, + match self.kind() { + SpecialFormCategory::TypeQualifier(_) + | SpecialFormCategory::Other(MiscSpecialForm::Unpack | MiscSpecialForm::TypeAlias) => { + false + } + SpecialFormCategory::Type + | SpecialFormCategory::Tuple + | SpecialFormCategory::Callable + | SpecialFormCategory::LegacyStdlibAlias(_) + | SpecialFormCategory::Other(_) => true, } } @@ -450,14 +417,6 @@ impl SpecialFormType { } } - #[expect(dead_code)] - pub(super) const fn as_type_qualifier(self) -> Option { - match self.kind() { - SpecialFormCategory::TypeQualifier(qualifier) => Some(qualifier), - _ => None, - } - } - /// Return `Some(KnownClass)` if this special form is an alias /// to a standard library class. pub(super) const fn aliased_stdlib_class(self) -> Option { @@ -465,59 +424,25 @@ impl SpecialFormType { SpecialFormCategory::LegacyStdlibAlias(alias) => Some(alias.aliased_class()), SpecialFormCategory::Tuple => Some(KnownClass::Tuple), SpecialFormCategory::Type => Some(KnownClass::Type), - _ => None, + // From ty's perspective, `collections.abc.Callable` and `typing.Callable` are the same object + SpecialFormCategory::Callable + | SpecialFormCategory::Other(_) + | SpecialFormCategory::TypeQualifier(_) => None, } } /// Return `true` if this special form is valid as the second argument /// to `issubclass()` and `isinstance()` calls. pub(super) const fn is_valid_isinstance_target(self) -> bool { - match self { - Self::Callable - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::FrozenSet - | Self::Dict - | Self::List - | Self::OrderedDict - | Self::Set - | Self::Tuple - | Self::Type - | Self::Protocol - | Self::Generic => true, - - Self::AlwaysFalsy - | Self::AlwaysTruthy - | Self::Annotated - | Self::Bottom - | Self::CallableTypeOf - | Self::ClassVar - | Self::Concatenate - | Self::Final - | Self::Intersection - | Self::Literal - | Self::LiteralString - | Self::Never - | Self::NoReturn - | Self::Not - | Self::ReadOnly - | Self::Required - | Self::TypeAlias - | Self::TypeGuard - | Self::NamedTuple - | Self::NotRequired - | Self::Optional - | Self::Top - | Self::TypeIs - | Self::TypedDict - | Self::TypingSelf - | Self::Union - | Self::Unknown - | Self::TypeOf - | Self::Any // can be used in `issubclass()` but not `isinstance()`. - | Self::Unpack => false, + match self.kind() { + SpecialFormCategory::LegacyStdlibAlias(_) + | SpecialFormCategory::Callable + | SpecialFormCategory::Tuple + | SpecialFormCategory::Type + | SpecialFormCategory::Other(MiscSpecialForm::Protocol | MiscSpecialForm::Generic) => { + true + } + SpecialFormCategory::TypeQualifier(_) | SpecialFormCategory::Other(_) => false, } } From 4ef7da90b6aae351ce7dce0c3508c0263205189d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 23 Feb 2026 11:47:59 +0000 Subject: [PATCH 07/11] make a match exhaustive --- .../src/types/class_base.rs | 2 +- .../src/types/special_form.rs | 35 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 55fa527b98045..7d0c99796e680 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -234,7 +234,7 @@ impl<'db> ClassBase<'db> { SpecialFormCategory::TypeQualifier(_) => None, - SpecialFormCategory::Other(alias) => match alias { + SpecialFormCategory::Other(form) => match form { special_form::MiscSpecialForm::Any => Some(Self::Dynamic(DynamicType::Any)), special_form::MiscSpecialForm::Unknown => Some(Self::unknown()), special_form::MiscSpecialForm::Protocol => Some(Self::Protocol), diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 6709eb3df9fad..eb2e8641f7d40 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -438,11 +438,36 @@ impl SpecialFormType { SpecialFormCategory::LegacyStdlibAlias(_) | SpecialFormCategory::Callable | SpecialFormCategory::Tuple - | SpecialFormCategory::Type - | SpecialFormCategory::Other(MiscSpecialForm::Protocol | MiscSpecialForm::Generic) => { - true - } - SpecialFormCategory::TypeQualifier(_) | SpecialFormCategory::Other(_) => false, + | SpecialFormCategory::Type => true, + SpecialFormCategory::TypeQualifier(_) => false, + SpecialFormCategory::Other(form) => match form { + MiscSpecialForm::Protocol | MiscSpecialForm::Generic => true, + MiscSpecialForm::Any + | MiscSpecialForm::Annotated + | MiscSpecialForm::Literal + | MiscSpecialForm::LiteralString + | MiscSpecialForm::Optional + | MiscSpecialForm::Union + | MiscSpecialForm::NoReturn + | MiscSpecialForm::Never + | MiscSpecialForm::Unknown + | MiscSpecialForm::AlwaysTruthy + | MiscSpecialForm::AlwaysFalsy + | MiscSpecialForm::Not + | MiscSpecialForm::Intersection + | MiscSpecialForm::TypeOf + | MiscSpecialForm::CallableTypeOf + | MiscSpecialForm::Top + | MiscSpecialForm::Bottom + | MiscSpecialForm::TypingSelf + | MiscSpecialForm::Concatenate + | MiscSpecialForm::Unpack + | MiscSpecialForm::TypeAlias + | MiscSpecialForm::TypeGuard + | MiscSpecialForm::TypedDict + | MiscSpecialForm::TypeIs + | MiscSpecialForm::NamedTuple => false, + }, } } From ba9e5243789244065d9b7d7f70724b21cd6c6545 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 23 Feb 2026 12:13:28 +0000 Subject: [PATCH 08/11] more cleanups --- .../types/infer/builder/type_expression.rs | 4 +- crates/ty_python_semantic/src/types/narrow.rs | 29 +++++--- .../src/types/special_form.rs | 73 ++++++++++++------- 3 files changed, 65 insertions(+), 41 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index ac31be26425c3..e411aaddab565 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1718,9 +1718,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { special_form::SpecialFormCategory::Type => { self.infer_subclass_of_type_expression(&subscript.slice) } - special_form::SpecialFormCategory::Callable => { - self.infer_callable_type(subscript) - } + special_form::SpecialFormCategory::Callable => self.infer_callable_type(subscript), special_form::SpecialFormCategory::TypeQualifier(qualifier) => { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let diag = builder.into_diagnostic(format_args!( diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 31f8b7fdffdfd..0a6d0abe3305f 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -16,7 +16,7 @@ use crate::types::typed_dict::{ }; use crate::types::{ CallableType, ClassLiteral, ClassType, IntersectionBuilder, IntersectionType, KnownClass, - KnownInstanceType, LiteralValueTypeKind, SpecialFormType, SubclassOfInner, SubclassOfType, + KnownInstanceType, LiteralValueTypeKind, SpecialFormCategory, SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types, }; @@ -228,17 +228,24 @@ impl ClassInfoConstraintFunction { ) } - // We don't have a good meta-type for `Callable`s right now, - // so only apply `isinstance()` narrowing, not `issubclass()` - Type::SpecialForm(SpecialFormType::Callable) - if self == ClassInfoConstraintFunction::IsInstance => - { - Some(Type::Callable(CallableType::unknown(db)).top_materialization(db)) - } + Type::SpecialForm(form) => match form.kind() { + SpecialFormCategory::LegacyStdlibAlias(alias) => { + self.generate_constraint(db, alias.aliased_class().to_class_literal(db)) + } + SpecialFormCategory::Tuple => { + self.generate_constraint(db, KnownClass::Tuple.to_class_literal(db)) + } + SpecialFormCategory::Type => { + self.generate_constraint(db, KnownClass::Type.to_class_literal(db)) + } + + // We don't have a good meta-type for `Callable`s right now, + // so only apply `isinstance()` narrowing, not `issubclass()` + SpecialFormCategory::Callable => (self == ClassInfoConstraintFunction::IsInstance) + .then(|| Type::Callable(CallableType::unknown(db)).top_materialization(db)), - Type::SpecialForm(special_form) => special_form - .aliased_stdlib_class() - .and_then(|class| self.generate_constraint(db, class.to_class_literal(db))), + SpecialFormCategory::TypeQualifier(_) | SpecialFormCategory::Other(_) => None, + }, Type::AlwaysFalsy | Type::AlwaysTruthy diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index eb2e8641f7d40..109e002879076 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -288,6 +288,7 @@ impl SpecialFormType { match self { // TypedDict can be called as a constructor to create TypedDict types Self::TypedDict + // Collection constructors are callable // TODO actually implement support for calling them | Self::ChainMap @@ -297,7 +298,16 @@ impl SpecialFormType { | Self::NamedTuple | Self::OrderedDict => true, - // All other special forms are not callable + // Unlike the aliases to `collections` classes, + // the aliases to builtin classes are *not* callable... + Self::List + | Self::Dict + | Self::Set + | Self::Tuple + | Self::Type + | Self::FrozenSet => false, + + // All other special forms are also not callable Self::Annotated | Self::Literal | Self::LiteralString @@ -305,12 +315,6 @@ impl SpecialFormType { | Self::Union | Self::NoReturn | Self::Never - | Self::Tuple - | Self::List - | Self::Dict - | Self::Set - | Self::FrozenSet - | Self::Type | Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy @@ -344,15 +348,44 @@ impl SpecialFormType { /// pub(super) const fn is_valid_in_type_expression(self) -> bool { match self.kind() { - SpecialFormCategory::TypeQualifier(_) - | SpecialFormCategory::Other(MiscSpecialForm::Unpack | MiscSpecialForm::TypeAlias) => { - false - } SpecialFormCategory::Type | SpecialFormCategory::Tuple | SpecialFormCategory::Callable - | SpecialFormCategory::LegacyStdlibAlias(_) - | SpecialFormCategory::Other(_) => true, + | SpecialFormCategory::LegacyStdlibAlias(_) => true, + SpecialFormCategory::TypeQualifier(_) => false, + + SpecialFormCategory::Other(form) => match form { + MiscSpecialForm::Any + | MiscSpecialForm::Annotated + | MiscSpecialForm::Literal + | MiscSpecialForm::LiteralString + | MiscSpecialForm::Optional + | MiscSpecialForm::Union + | MiscSpecialForm::NoReturn + | MiscSpecialForm::Never + | MiscSpecialForm::Unknown + | MiscSpecialForm::AlwaysTruthy + | MiscSpecialForm::AlwaysFalsy + | MiscSpecialForm::Not + | MiscSpecialForm::Intersection + | MiscSpecialForm::TypeOf + | MiscSpecialForm::CallableTypeOf + | MiscSpecialForm::Top + | MiscSpecialForm::Bottom + | MiscSpecialForm::Concatenate + | MiscSpecialForm::TypeGuard + | MiscSpecialForm::TypedDict + | MiscSpecialForm::TypeIs + | MiscSpecialForm::NamedTuple + | MiscSpecialForm::TypingSelf => true, + + MiscSpecialForm::Generic + | MiscSpecialForm::Protocol + | MiscSpecialForm::TypeAlias => false, + + // TODO: seems incorrect? + MiscSpecialForm::Unpack => false, + }, } } @@ -417,20 +450,6 @@ impl SpecialFormType { } } - /// Return `Some(KnownClass)` if this special form is an alias - /// to a standard library class. - pub(super) const fn aliased_stdlib_class(self) -> Option { - match self.kind() { - SpecialFormCategory::LegacyStdlibAlias(alias) => Some(alias.aliased_class()), - SpecialFormCategory::Tuple => Some(KnownClass::Tuple), - SpecialFormCategory::Type => Some(KnownClass::Type), - // From ty's perspective, `collections.abc.Callable` and `typing.Callable` are the same object - SpecialFormCategory::Callable - | SpecialFormCategory::Other(_) - | SpecialFormCategory::TypeQualifier(_) => None, - } - } - /// Return `true` if this special form is valid as the second argument /// to `issubclass()` and `isinstance()` calls. pub(super) const fn is_valid_isinstance_target(self) -> bool { From 5b6f49accb0fd4bd071e747ebd815cd6b329ad50 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 13:59:28 +0000 Subject: [PATCH 09/11] Restructure SpecialFormType as a nested enum Instead of having a flat SpecialFormType enum with a separate SpecialFormCategory enum that duplicates part of the structure via a kind() method, embed the subcategories directly into SpecialFormType itself as nested enum variants: - SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias) - SpecialFormType::TypeQualifier(TypeQualifier) This eliminates SpecialFormCategory, MiscSpecialForm, and the kind() mapping method, along with several From impls that existed only to convert between the parallel structures. Callers now match directly on SpecialFormType variants instead of calling .kind() first. This is an alternative to the SpecialFormCategory approach, as suggested in https://github.com/astral-sh/ruff/pull/23513#issuecomment-3944703095 https://claude.ai/code/session_01AgLYCKoBuLEDsmkn1cnmvZ --- crates/ty_python_semantic/src/types.rs | 33 +- .../src/types/class_base.rs | 115 ++- .../src/types/infer/builder.rs | 72 +- .../infer/builder/annotation_expression.rs | 65 +- .../types/infer/builder/type_expression.rs | 146 ++- crates/ty_python_semantic/src/types/narrow.rs | 14 +- .../src/types/special_form.rs | 904 +++++++----------- 7 files changed, 553 insertions(+), 796 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e737d36469b83..50fe94eb97ba0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -73,7 +73,7 @@ pub(crate) use crate::types::narrow::{ use crate::types::newtype::NewType; pub(crate) use crate::types::signatures::{Parameter, Parameters}; use crate::types::signatures::{ParameterForm, walk_signature}; -use crate::types::special_form::{SpecialFormCategory, TypeQualifier}; +use crate::types::special_form::TypeQualifier; use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder}; use crate::types::typed_dict::TypedDictField; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; @@ -5990,34 +5990,9 @@ impl<'db> Type<'db> { KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), }, - Type::SpecialForm(special_form) => match special_form.kind() { - // We treat `typing.Type` exactly the same as `builtins.type`: - SpecialFormCategory::Type => Ok(KnownClass::Type.to_instance(db)), - SpecialFormCategory::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), - SpecialFormCategory::Callable => Ok(Type::Callable(CallableType::unknown(db))), - SpecialFormCategory::LegacyStdlibAlias(alias) => { - Ok(alias.aliased_class().to_instance(db)) - } - SpecialFormCategory::Other(form) => { - form.in_type_expression(db, scope_id, typevar_binding_context) - } - SpecialFormCategory::TypeQualifier(qualifier) => { - let err = match qualifier { - TypeQualifier::Final | TypeQualifier::ClassVar => { - InvalidTypeExpression::TypeQualifier(qualifier) - } - TypeQualifier::ReadOnly - | TypeQualifier::NotRequired - | TypeQualifier::Required => { - InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) - } - }; - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![err], - fallback_type: Type::unknown(), - }) - } - }, + Type::SpecialForm(special_form) => { + special_form.in_type_expression(db, scope_id, typevar_binding_context) + } Type::Union(union) => { let mut builder = UnionBuilder::new(db); diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 7d0c99796e680..a8f1ea4722fe0 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,7 +1,6 @@ use crate::types::class::CodeGeneratorKind; use crate::types::generics::{ApplySpecialization, Specialization}; use crate::types::mro::MroIterator; -use crate::types::special_form::{self, SpecialFormCategory}; use crate::types::tuple::TupleType; use crate::types::{ ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType, @@ -212,72 +211,70 @@ impl<'db> ClassBase<'db> { } }, - Type::SpecialForm(special_form) => match special_form.kind() { - SpecialFormCategory::LegacyStdlibAlias(alias) => { - Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass) - } - - SpecialFormCategory::Callable => Self::try_from_type( - db, - todo_type!("Support for Callable as a base class"), - subclass, - ), - - SpecialFormCategory::Tuple => { - Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass) + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::TypeQualifier(_) => None, + + SpecialFormType::Annotated + | SpecialFormType::Literal + | SpecialFormType::LiteralString + | SpecialFormType::Union + | SpecialFormType::NoReturn + | SpecialFormType::Never + | SpecialFormType::TypeGuard + | SpecialFormType::TypeIs + | SpecialFormType::TypingSelf + | SpecialFormType::Unpack + | SpecialFormType::Concatenate + | SpecialFormType::TypeAlias + | SpecialFormType::Optional + | SpecialFormType::Not + | SpecialFormType::Top + | SpecialFormType::Bottom + | SpecialFormType::Intersection + | SpecialFormType::TypeOf + | SpecialFormType::CallableTypeOf + | SpecialFormType::AlwaysTruthy + | SpecialFormType::AlwaysFalsy => None, + + SpecialFormType::Any => Some(Self::Dynamic(DynamicType::Any)), + SpecialFormType::Unknown => Some(Self::unknown()), + SpecialFormType::Protocol => Some(Self::Protocol), + SpecialFormType::Generic => Some(Self::Generic), + SpecialFormType::TypedDict => Some(Self::TypedDict), + + SpecialFormType::NamedTuple => { + let class = subclass?.as_static()?; + let fields = class.own_fields(db, None, CodeGeneratorKind::NamedTuple); + Self::try_from_type( + db, + TupleType::heterogeneous( + db, + fields.values().map(|field| field.declared_ty), + )? + .to_class_type(db) + .into(), + subclass, + ) } // TODO: Classes inheriting from `typing.Type` also have `Generic` in their MRO - SpecialFormCategory::Type => { + SpecialFormType::Type => { Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) } - SpecialFormCategory::TypeQualifier(_) => None, + SpecialFormType::Tuple => { + Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass) + } - SpecialFormCategory::Other(form) => match form { - special_form::MiscSpecialForm::Any => Some(Self::Dynamic(DynamicType::Any)), - special_form::MiscSpecialForm::Unknown => Some(Self::unknown()), - special_form::MiscSpecialForm::Protocol => Some(Self::Protocol), - special_form::MiscSpecialForm::Generic => Some(Self::Generic), - special_form::MiscSpecialForm::TypedDict => Some(Self::TypedDict), + SpecialFormType::LegacyStdlibAlias(alias) => { + Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass) + } - special_form::MiscSpecialForm::NamedTuple => { - let class = subclass?.as_static()?; - let fields = class.own_fields(db, None, CodeGeneratorKind::NamedTuple); - Self::try_from_type( - db, - TupleType::heterogeneous( - db, - fields.values().map(|field| field.declared_ty), - )? - .to_class_type(db) - .into(), - subclass, - ) - } - - special_form::MiscSpecialForm::AlwaysFalsy - | special_form::MiscSpecialForm::AlwaysTruthy - | special_form::MiscSpecialForm::TypeOf - | special_form::MiscSpecialForm::CallableTypeOf - | special_form::MiscSpecialForm::TypeIs - | special_form::MiscSpecialForm::TypingSelf - | special_form::MiscSpecialForm::Not - | special_form::MiscSpecialForm::Top - | special_form::MiscSpecialForm::Bottom - | special_form::MiscSpecialForm::Intersection - | special_form::MiscSpecialForm::Literal - | special_form::MiscSpecialForm::LiteralString - | special_form::MiscSpecialForm::Annotated - | special_form::MiscSpecialForm::TypeAlias - | special_form::MiscSpecialForm::Optional - | special_form::MiscSpecialForm::Unpack - | special_form::MiscSpecialForm::Concatenate - | special_form::MiscSpecialForm::Never - | special_form::MiscSpecialForm::NoReturn - | special_form::MiscSpecialForm::Union - | special_form::MiscSpecialForm::TypeGuard => None, - }, + SpecialFormType::Callable => Self::try_from_type( + db, + todo_type!("Support for Callable as a base class"), + subclass, + ), }, } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b72c4690af96e..41a4db1fd09a7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -119,7 +119,7 @@ use crate::types::generics::{ use crate::types::infer::nearest_enclosing_function; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; -use crate::types::special_form::{AliasSpec, MiscSpecialForm}; +use crate::types::special_form::AliasSpec; use crate::types::subclass_of::SubclassOfInner; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleSpecBuilder, TupleType}; @@ -133,13 +133,12 @@ use crate::types::{ IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, - Parameters, Signature, SpecialFormCategory, SpecialFormType, StaticClassLiteral, - SubclassOfType, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, - TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, - TypeVarConstraints, TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, - TypeVarVariance, TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, any_over_type, - binding_type, definition_expression_type, infer_complete_scope_types, infer_scope_types, - todo_type, + Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, + TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, TypeVarDefaultEvaluation, + TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, + UnionType, UnionTypeInstance, any_over_type, binding_type, definition_expression_type, + infer_complete_scope_types, infer_scope_types, todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; @@ -15935,37 +15934,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } - Type::SpecialForm(special_form) => match special_form.kind() { - SpecialFormCategory::Tuple => { + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Tuple => { return tuple_generic_alias( self.db(), self.infer_tuple_type_expression(subscript), ); } - SpecialFormCategory::Other(MiscSpecialForm::Literal) => { - match self.infer_literal_parameter_type(slice) { - Ok(result) => { - return Type::KnownInstance(KnownInstanceType::Literal( - InternedType::new(self.db(), result), - )); - } - Err(nodes) => { - for node in nodes { - let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, node) - else { - continue; - }; - builder.into_diagnostic( - "Type arguments for `Literal` must be `None`, \ + SpecialFormType::Literal => match self.infer_literal_parameter_type(slice) { + Ok(result) => { + return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( + self.db(), + result, + ))); + } + Err(nodes) => { + for node in nodes { + let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) + else { + continue; + }; + builder.into_diagnostic( + "Type arguments for `Literal` must be `None`, \ a literal value (int, bool, str, or bytes), or an enum member", - ); - } - return Type::unknown(); + ); } + return Type::unknown(); } - } - SpecialFormCategory::Other(MiscSpecialForm::Annotated) => { + }, + SpecialFormType::Annotated => { let ast::Expr::Tuple(ast::ExprTuple { elts: ref arguments, .. @@ -15999,7 +15996,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ty, ))); } - SpecialFormCategory::Other(MiscSpecialForm::Optional) => { + SpecialFormType::Optional => { let db = self.db(); if matches!(**slice, ast::Expr::Tuple(_)) @@ -16026,7 +16023,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ), )); } - SpecialFormCategory::Other(MiscSpecialForm::Union) => { + SpecialFormType::Union => { let db = self.db(); match **slice { @@ -16062,14 +16059,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } - SpecialFormCategory::Type => { + SpecialFormType::Type => { // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` let argument_ty = self.infer_type_expression(slice); return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( InternedType::new(self.db(), argument_ty), )); } - SpecialFormCategory::Callable => { + SpecialFormType::Callable => { let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { &*tuple.elts } else { @@ -16128,8 +16125,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Type::KnownInstance(KnownInstanceType::Callable(callable)); } - SpecialFormCategory::Other(_) => {} - SpecialFormCategory::LegacyStdlibAlias(alias) => { + SpecialFormType::LegacyStdlibAlias(alias) => { let AliasSpec { class, expected_argument_number, @@ -16169,7 +16165,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(Type::from) .unwrap_or_else(Type::unknown); } - SpecialFormCategory::TypeQualifier(_) => {} + _ => {} }, Type::KnownInstance( diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index fe521ba3860f1..7a245bd545f33 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -6,7 +6,6 @@ use crate::types::diagnostic::{ INVALID_TYPE_FORM, REDUNDANT_FINAL_CLASSVAR, report_invalid_arguments_to_annotated, }; use crate::types::infer::nearest_enclosing_class; -use crate::types::special_form::{MiscSpecialForm, SpecialFormCategory}; use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, }; @@ -91,37 +90,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder: &TypeInferenceBuilder<'db, '_>, pep_613_policy: PEP613Policy, ) -> TypeAndQualifiers<'db> { - match ty { - Type::SpecialForm(special_form) => match special_form.kind() { - SpecialFormCategory::TypeQualifier(qualifier) => TypeAndQualifiers::new( + let special_case = match ty { + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::TypeQualifier(qualifier) => Some(TypeAndQualifiers::new( Type::unknown(), TypeOrigin::Declared, TypeQualifiers::from(qualifier), - ), - SpecialFormCategory::Other(MiscSpecialForm::TypeAlias) - if pep_613_policy == PEP613Policy::Allowed => - { - TypeAndQualifiers::declared(ty) + )), + SpecialFormType::TypeAlias if pep_613_policy == PEP613Policy::Allowed => { + Some(TypeAndQualifiers::declared(ty)) } - SpecialFormCategory::Type - | SpecialFormCategory::Tuple - | SpecialFormCategory::Callable - | SpecialFormCategory::LegacyStdlibAlias(_) - | SpecialFormCategory::Other(_) => TypeAndQualifiers::declared( - ty.default_specialize(builder.db()) - .in_type_expression( - builder.db(), - builder.scope(), - builder.typevar_binding_context, - ) - .unwrap_or_else(|error| { - error.into_fallback_type( - &builder.context, - annotation, - builder.is_reachable(annotation), - ) - }), - ), + _ => None, }, // Conditional import of `typing.TypeAlias` or `typing_extensions.TypeAlias` on a // Python version where the former doesn't exist. @@ -134,7 +113,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) }) => { - TypeAndQualifiers::declared(Type::SpecialForm(SpecialFormType::TypeAlias)) + Some(TypeAndQualifiers::declared(Type::SpecialForm( + SpecialFormType::TypeAlias, + ))) } Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => { if let Some(builder) = @@ -143,13 +124,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder .into_diagnostic("`InitVar` may not be used without a type argument"); } - TypeAndQualifiers::new( + Some(TypeAndQualifiers::new( Type::unknown(), TypeOrigin::Declared, TypeQualifiers::INIT_VAR, - ) + )) } - _ => TypeAndQualifiers::declared( + _ => None, + }; + + special_case.unwrap_or_else(|| { + TypeAndQualifiers::declared( ty.default_specialize(builder.db()) .in_type_expression( builder.db(), @@ -163,8 +148,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.is_reachable(annotation), ) }), - ), - } + ) + }) } // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression @@ -231,8 +216,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let slice = &**slice; match value_ty { - Type::SpecialForm(special_form) => match special_form.kind() { - SpecialFormCategory::Other(MiscSpecialForm::Annotated) => { + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Annotated => { // This branch is similar to the corresponding branch in // `infer_parameterized_special_form_type_expression`, but // `Annotated[…]` can appear both in annotation expressions and in @@ -278,7 +263,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } } - SpecialFormCategory::TypeQualifier(qualifier) => { + SpecialFormType::TypeQualifier(qualifier) => { let arguments = if let ast::Expr::Tuple(tuple) = slice { &*tuple.elts } else { @@ -348,11 +333,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } type_and_qualifiers } - SpecialFormCategory::Type - | SpecialFormCategory::Tuple - | SpecialFormCategory::Callable - | SpecialFormCategory::LegacyStdlibAlias(_) - | SpecialFormCategory::Other(_) => TypeAndQualifiers::declared( + _ => TypeAndQualifiers::declared( self.infer_subscript_type_expression_no_store( subscript, slice, value_ty, ), diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index e411aaddab565..b07ae6dd88bcf 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -11,7 +11,7 @@ use crate::types::diagnostic::{ use crate::types::generics::bind_typevar; use crate::types::infer::builder::InnerExpressionInferenceState; use crate::types::signatures::Signature; -use crate::types::special_form::{self, AliasSpec, LegacyStdlibAlias}; +use crate::types::special_form::{AliasSpec, LegacyStdlibAlias}; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::{ @@ -1306,16 +1306,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { callable_type } - fn infer_parameterized_non_stdlib_alias_special_form( + fn infer_parameterized_special_form_type_expression( &mut self, subscript: &ast::ExprSubscript, - special_form: special_form::MiscSpecialForm, + special_form: SpecialFormType, ) -> Type<'db> { let db = self.db(); let arguments_slice = &*subscript.slice; - match special_form { - special_form::MiscSpecialForm::Annotated => { + SpecialFormType::Annotated => { let ty = self .infer_subscript_load_impl( Type::SpecialForm(SpecialFormType::Annotated), @@ -1330,29 +1329,27 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ty } - special_form::MiscSpecialForm::Literal => { - match self.infer_literal_parameter_type(arguments_slice) { - Ok(ty) => ty, - Err(nodes) => { - for node in nodes { - let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) - else { - continue; - }; - builder.into_diagnostic( - "Type arguments for `Literal` must be `None`, \ + SpecialFormType::Literal => match self.infer_literal_parameter_type(arguments_slice) { + Ok(ty) => ty, + Err(nodes) => { + for node in nodes { + let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) + else { + continue; + }; + builder.into_diagnostic( + "Type arguments for `Literal` must be `None`, \ a literal value (int, bool, str, or bytes), or an enum member", - ); - } - Type::unknown() + ); } + Type::unknown() } - } - special_form::MiscSpecialForm::Optional => { + }, + SpecialFormType::Optional => { let param_type = self.infer_type_expression(arguments_slice); UnionType::from_elements_leave_aliases(db, [param_type, Type::none(db)]) } - special_form::MiscSpecialForm::Union => match arguments_slice { + SpecialFormType::Union => match arguments_slice { ast::Expr::Tuple(t) => { let union_ty = UnionType::from_elements_leave_aliases( db, @@ -1363,9 +1360,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } _ => self.infer_type_expression(arguments_slice), }, + SpecialFormType::Callable => self.infer_callable_type(subscript), // `ty_extensions` special forms - special_form::MiscSpecialForm::Not => { + SpecialFormType::Not => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1392,7 +1390,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } negated_type } - special_form::MiscSpecialForm::Intersection => { + SpecialFormType::Intersection => { let elements = match arguments_slice { ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), element => Either::Right(std::iter::once(element)), @@ -1409,7 +1407,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ty } - special_form::MiscSpecialForm::Top => { + SpecialFormType::Top => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1433,7 +1431,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; arg.top_materialization(db) } - special_form::MiscSpecialForm::Bottom => { + SpecialFormType::Bottom => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1457,7 +1455,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; arg.bottom_materialization(db) } - special_form::MiscSpecialForm::TypeOf => { + SpecialFormType::TypeOf => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1486,7 +1484,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { type_of_type } - special_form::MiscSpecialForm::CallableTypeOf => { + SpecialFormType::CallableTypeOf => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1539,14 +1537,27 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } callable_type } - special_form::MiscSpecialForm::TypeIs => match arguments_slice { + SpecialFormType::LegacyStdlibAlias(alias) => { + self.infer_parameterized_legacy_typing_alias(subscript, alias) + } + SpecialFormType::TypeQualifier(qualifier) => { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let diag = builder.into_diagnostic(format_args!( + "Type qualifier `{qualifier}` is not allowed in type expressions \ + (only in annotation expressions)", + )); + diagnostic::add_type_expression_reference_link(diag); + } + self.infer_type_expression(arguments_slice) + } + SpecialFormType::TypeIs => match arguments_slice { ast::Expr::Tuple(_) => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - let diag = builder.into_diagnostic(format_args!( - "Special form `{special_form}` expected exactly one type parameter", - )); + let diag = builder.into_diagnostic( + "Special form `typing.TypeIs` expected exactly one type parameter", + ); diagnostic::add_type_expression_reference_link(diag); } @@ -1564,14 +1575,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .top_materialization(self.db()), ), }, - special_form::MiscSpecialForm::TypeGuard => match arguments_slice { + SpecialFormType::TypeGuard => match arguments_slice { ast::Expr::Tuple(_) => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - let diag = builder.into_diagnostic(format_args!( - "Special form `{special_form}` expected exactly one type parameter", - )); + let diag = builder.into_diagnostic( + "Special form `typing.TypeGuard` expected exactly one type parameter", + ); diagnostic::add_type_expression_reference_link(diag); } @@ -1584,7 +1595,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_type_expression(arguments_slice), ), }, - special_form::MiscSpecialForm::Concatenate => { + SpecialFormType::Concatenate => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { &*tuple.elts } else { @@ -1609,14 +1620,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } inferred_type } - special_form::MiscSpecialForm::Unpack => { + SpecialFormType::Unpack => { self.infer_type_expression(arguments_slice); todo_type!("`Unpack[]` special form") } - special_form::MiscSpecialForm::NoReturn - | special_form::MiscSpecialForm::Never - | special_form::MiscSpecialForm::AlwaysTruthy - | special_form::MiscSpecialForm::AlwaysFalsy => { + SpecialFormType::NoReturn + | SpecialFormType::Never + | SpecialFormType::AlwaysTruthy + | SpecialFormType::AlwaysFalsy => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1626,12 +1637,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - special_form::MiscSpecialForm::TypingSelf - | special_form::MiscSpecialForm::TypeAlias - | special_form::MiscSpecialForm::TypedDict - | special_form::MiscSpecialForm::Unknown - | special_form::MiscSpecialForm::Any - | special_form::MiscSpecialForm::NamedTuple => { + SpecialFormType::TypingSelf + | SpecialFormType::TypeAlias + | SpecialFormType::TypedDict + | SpecialFormType::Unknown + | SpecialFormType::Any + | SpecialFormType::NamedTuple => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1641,7 +1652,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - special_form::MiscSpecialForm::LiteralString => { + SpecialFormType::LiteralString => { let arguments = self.infer_expression(arguments_slice, TypeContext::default()); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1688,7 +1699,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - special_form::MiscSpecialForm::Generic | special_form::MiscSpecialForm::Protocol => { + SpecialFormType::Type => self.infer_subclass_of_type_expression(arguments_slice), + SpecialFormType::Tuple => Type::tuple(self.infer_tuple_type_expression(subscript)), + SpecialFormType::Generic | SpecialFormType::Protocol => { self.infer_expression(arguments_slice, TypeContext::default()); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( @@ -1700,39 +1713,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - fn infer_parameterized_special_form_type_expression( - &mut self, - subscript: &ast::ExprSubscript, - special_form: SpecialFormType, - ) -> Type<'db> { - match special_form.kind() { - special_form::SpecialFormCategory::LegacyStdlibAlias(alias) => { - self.infer_parameterized_legacy_typing_alias(subscript, alias) - } - special_form::SpecialFormCategory::Other(alias) => { - self.infer_parameterized_non_stdlib_alias_special_form(subscript, alias) - } - special_form::SpecialFormCategory::Tuple => { - Type::tuple(self.infer_tuple_type_expression(subscript)) - } - special_form::SpecialFormCategory::Type => { - self.infer_subclass_of_type_expression(&subscript.slice) - } - special_form::SpecialFormCategory::Callable => self.infer_callable_type(subscript), - special_form::SpecialFormCategory::TypeQualifier(qualifier) => { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - let diag = builder.into_diagnostic(format_args!( - "Type qualifier `{qualifier}` is not allowed in type expressions \ - (only in annotation expressions)", - )); - diagnostic::add_type_expression_reference_link(diag); - } - self.infer_type_expression(&subscript.slice); - Type::unknown() - } - } - } - pub(crate) fn infer_literal_parameter_type<'param>( &mut self, parameters: &'param ast::Expr, diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 0a6d0abe3305f..dc98dbf579534 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -16,7 +16,7 @@ use crate::types::typed_dict::{ }; use crate::types::{ CallableType, ClassLiteral, ClassType, IntersectionBuilder, IntersectionType, KnownClass, - KnownInstanceType, LiteralValueTypeKind, SpecialFormCategory, SubclassOfInner, SubclassOfType, + KnownInstanceType, LiteralValueTypeKind, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types, }; @@ -228,23 +228,23 @@ impl ClassInfoConstraintFunction { ) } - Type::SpecialForm(form) => match form.kind() { - SpecialFormCategory::LegacyStdlibAlias(alias) => { + Type::SpecialForm(form) => match form { + SpecialFormType::LegacyStdlibAlias(alias) => { self.generate_constraint(db, alias.aliased_class().to_class_literal(db)) } - SpecialFormCategory::Tuple => { + SpecialFormType::Tuple => { self.generate_constraint(db, KnownClass::Tuple.to_class_literal(db)) } - SpecialFormCategory::Type => { + SpecialFormType::Type => { self.generate_constraint(db, KnownClass::Type.to_class_literal(db)) } // We don't have a good meta-type for `Callable`s right now, // so only apply `isinstance()` narrowing, not `issubclass()` - SpecialFormCategory::Callable => (self == ClassInfoConstraintFunction::IsInstance) + SpecialFormType::Callable => (self == ClassInfoConstraintFunction::IsInstance) .then(|| Type::Callable(CallableType::unknown(db)).top_materialization(db)), - SpecialFormCategory::TypeQualifier(_) | SpecialFormCategory::Other(_) => None, + _ => None, }, Type::AlwaysFalsy diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 109e002879076..074f830bdee7d 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -7,36 +7,55 @@ use crate::semantic_index::place::ScopedPlaceId; use crate::semantic_index::{ FileScopeId, definition::Definition, place_table, scope::ScopeId, semantic_index, use_def_map, }; -use crate::types::TypeDefinition; use crate::types::TypeQualifiers; +use crate::types::{CallableType, TypeDefinition}; use crate::types::{ IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, generics::typing_self, infer::nearest_enclosing_class, }; use ruff_db::files::File; -use std::str::FromStr; use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; /// Enumeration of specific runtime symbols that are special enough /// that they can each be considered to inhabit a unique type. /// +/// The enum uses a nested structure: variants that fall into well-defined subcategories +/// (legacy stdlib aliases and type qualifiers) are represented as nested enums, +/// while other special forms that each require unique handling remain as direct variants. +/// /// # Ordering /// /// Ordering is stable and should be the same between runs. #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Hash, - salsa::Update, - PartialOrd, - Ord, - strum_macros::EnumString, - get_size2::GetSize, + Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, )] pub enum SpecialFormType { + /// Special forms that are simple aliases to classes elsewhere in the standard library. + LegacyStdlibAlias(LegacyStdlibAlias), + + /// Special forms that are type qualifiers + TypeQualifier(TypeQualifier), + + /// The special form `typing.Tuple`. + /// + /// While this is technically an alias to `builtins.tuple`, it requires special handling + /// for type-expression parsing. + Tuple, + + /// The special form `typing.Type`. + /// + /// While this is technically an alias to `builtins.type`, it requires special handling + /// for type-expression parsing. + Type, + + /// The special form `Callable`. + /// + /// While `typing.Callable` aliases `collections.abc.Callable`, we view both objects + /// as inhabiting the same special form type internally. Moreover, `Callable` requires + /// special handling for both type-expression parsing and `isinstance`/`issubclass` + /// narrowing. + Callable, + Any, /// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`) Annotated, @@ -52,28 +71,6 @@ pub enum SpecialFormType { NoReturn, /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) Never, - /// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`) - Tuple, - /// The symbol `typing.List` (which can also be found as `typing_extensions.List`) - List, - /// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`) - Dict, - /// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`) - Set, - /// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`) - FrozenSet, - /// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`) - ChainMap, - /// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`) - Counter, - /// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`) - DefaultDict, - /// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`) - Deque, - /// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`) - OrderedDict, - /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) - Type, /// The symbol `ty_extensions.Unknown` Unknown, /// The symbol `ty_extensions.AlwaysTruthy` @@ -92,24 +89,12 @@ pub enum SpecialFormType { Top, /// The symbol `ty_extensions.Bottom` Bottom, - /// The symbol `typing.Callable` - /// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`) - Callable, /// The symbol `typing.Self` (which can also be found as `typing_extensions.Self`) - #[strum(serialize = "Self")] TypingSelf, - /// The symbol `typing.Final` (which can also be found as `typing_extensions.Final`) - Final, - /// The symbol `typing.ClassVar` (which can also be found as `typing_extensions.ClassVar`) - ClassVar, /// The symbol `typing.Concatenate` (which can also be found as `typing_extensions.Concatenate`) Concatenate, /// The symbol `typing.Unpack` (which can also be found as `typing_extensions.Unpack`) Unpack, - /// The symbol `typing.Required` (which can also be found as `typing_extensions.Required`) - Required, - /// The symbol `typing.NotRequired` (which can also be found as `typing_extensions.NotRequired`) - NotRequired, /// The symbol `typing.TypeAlias` (which can also be found as `typing_extensions.TypeAlias`) TypeAlias, /// The symbol `typing.TypeGuard` (which can also be found as `typing_extensions.TypeGuard`) @@ -118,8 +103,6 @@ pub enum SpecialFormType { TypedDict, /// The symbol `typing.TypeIs` (which can also be found as `typing_extensions.TypeIs`) TypeIs, - /// The symbol `typing.ReadOnly` (which can also be found as `typing_extensions.ReadOnly`) - ReadOnly, /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) /// @@ -153,13 +136,9 @@ impl SpecialFormType { | Self::Tuple | Self::Type | Self::TypingSelf - | Self::Final - | Self::ClassVar | Self::Callable | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypedDict @@ -170,7 +149,7 @@ impl SpecialFormType { | Self::Bottom | Self::Intersection | Self::CallableTypeOf - | Self::ReadOnly => KnownClass::SpecialForm, + | Self::TypeQualifier(_) => KnownClass::SpecialForm, // Typeshed says it's an instance of `_SpecialForm`, // but then we wouldn't recognise things like `issubclass(`X, Protocol)` @@ -179,15 +158,7 @@ impl SpecialFormType { Self::Generic | Self::Any => KnownClass::Type, - Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::Deque - | Self::ChainMap - | Self::OrderedDict => KnownClass::StdlibAlias, + Self::LegacyStdlibAlias(_) => KnownClass::StdlibAlias, Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy => KnownClass::Object, @@ -214,28 +185,71 @@ impl SpecialFormType { file: File, symbol_name: &str, ) -> Option { - let candidate = Self::from_str(symbol_name).ok()?; + let candidate = Self::from_name(symbol_name)?; candidate .check_module(file_to_module(db, file)?.known(db)?) .then_some(candidate) } + /// Parse a `SpecialFormType` from its runtime symbol name. + fn from_name(name: &str) -> Option { + Some(match name { + "Any" => Self::Any, + "Annotated" => Self::Annotated, + "Literal" => Self::Literal, + "LiteralString" => Self::LiteralString, + "Optional" => Self::Optional, + "Union" => Self::Union, + "NoReturn" => Self::NoReturn, + "Never" => Self::Never, + "Tuple" => Self::Tuple, + "Type" => Self::Type, + "Self" => Self::TypingSelf, + "Final" => Self::TypeQualifier(TypeQualifier::Final), + "ClassVar" => Self::TypeQualifier(TypeQualifier::ClassVar), + "Callable" => Self::Callable, + "Concatenate" => Self::Concatenate, + "Unpack" => Self::Unpack, + "Required" => Self::TypeQualifier(TypeQualifier::Required), + "NotRequired" => Self::TypeQualifier(TypeQualifier::NotRequired), + "TypeAlias" => Self::TypeAlias, + "TypeGuard" => Self::TypeGuard, + "TypedDict" => Self::TypedDict, + "TypeIs" => Self::TypeIs, + "ReadOnly" => Self::TypeQualifier(TypeQualifier::ReadOnly), + "List" => Self::LegacyStdlibAlias(LegacyStdlibAlias::List), + "Dict" => Self::LegacyStdlibAlias(LegacyStdlibAlias::Dict), + "DefaultDict" => Self::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict), + "Set" => Self::LegacyStdlibAlias(LegacyStdlibAlias::Set), + "FrozenSet" => Self::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet), + "Counter" => Self::LegacyStdlibAlias(LegacyStdlibAlias::Counter), + "Deque" => Self::LegacyStdlibAlias(LegacyStdlibAlias::Deque), + "ChainMap" => Self::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap), + "OrderedDict" => Self::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict), + "Unknown" => Self::Unknown, + "AlwaysTruthy" => Self::AlwaysTruthy, + "AlwaysFalsy" => Self::AlwaysFalsy, + "Not" => Self::Not, + "Intersection" => Self::Intersection, + "TypeOf" => Self::TypeOf, + "CallableTypeOf" => Self::CallableTypeOf, + "Top" => Self::Top, + "Bottom" => Self::Bottom, + "Protocol" => Self::Protocol, + "Generic" => Self::Generic, + "NamedTuple" => Self::NamedTuple, + _ => return None, + }) + } + /// Return `true` if `module` is a module from which this `SpecialFormType` variant can validly originate. /// /// Most variants can only exist in one module, which is the same as `self.class().canonical_module(db)`. /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. pub(super) fn check_module(self, module: KnownModule) -> bool { match self { - Self::ClassVar - | Self::Deque - | Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::ChainMap - | Self::OrderedDict + Self::TypeQualifier(TypeQualifier::ClassVar) + | Self::LegacyStdlibAlias(_) | Self::Optional | Self::Union | Self::NoReturn @@ -248,11 +262,14 @@ impl SpecialFormType { | Self::Literal | Self::LiteralString | Self::Never - | Self::Final + | Self::TypeQualifier( + TypeQualifier::Final + | TypeQualifier::Required + | TypeQualifier::NotRequired + | TypeQualifier::ReadOnly, + ) | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypedDict @@ -260,8 +277,7 @@ impl SpecialFormType { | Self::TypingSelf | Self::Protocol | Self::NamedTuple - | Self::Any - | Self::ReadOnly => { + | Self::Any => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } @@ -291,21 +307,25 @@ impl SpecialFormType { // Collection constructors are callable // TODO actually implement support for calling them - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::NamedTuple - | Self::OrderedDict => true, + | Self::LegacyStdlibAlias( + LegacyStdlibAlias::ChainMap + | LegacyStdlibAlias::Counter + | LegacyStdlibAlias::DefaultDict + | LegacyStdlibAlias::Deque + | LegacyStdlibAlias::OrderedDict + ) + | Self::NamedTuple => true, // Unlike the aliases to `collections` classes, // the aliases to builtin classes are *not* callable... - Self::List - | Self::Dict - | Self::Set + Self::LegacyStdlibAlias( + LegacyStdlibAlias::List + | LegacyStdlibAlias::Dict + | LegacyStdlibAlias::Set + | LegacyStdlibAlias::FrozenSet + ) | Self::Tuple - | Self::Type - | Self::FrozenSet => false, + | Self::Type => false, // All other special forms are also not callable Self::Annotated @@ -326,16 +346,12 @@ impl SpecialFormType { | Self::CallableTypeOf | Self::Callable | Self::TypingSelf - | Self::Final - | Self::ClassVar + | Self::TypeQualifier(_) | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypeIs - | Self::ReadOnly | Self::Protocol | Self::Any | Self::Generic => false, @@ -347,247 +363,162 @@ impl SpecialFormType { /// specification for more details: /// pub(super) const fn is_valid_in_type_expression(self) -> bool { - match self.kind() { - SpecialFormCategory::Type - | SpecialFormCategory::Tuple - | SpecialFormCategory::Callable - | SpecialFormCategory::LegacyStdlibAlias(_) => true, - SpecialFormCategory::TypeQualifier(_) => false, - - SpecialFormCategory::Other(form) => match form { - MiscSpecialForm::Any - | MiscSpecialForm::Annotated - | MiscSpecialForm::Literal - | MiscSpecialForm::LiteralString - | MiscSpecialForm::Optional - | MiscSpecialForm::Union - | MiscSpecialForm::NoReturn - | MiscSpecialForm::Never - | MiscSpecialForm::Unknown - | MiscSpecialForm::AlwaysTruthy - | MiscSpecialForm::AlwaysFalsy - | MiscSpecialForm::Not - | MiscSpecialForm::Intersection - | MiscSpecialForm::TypeOf - | MiscSpecialForm::CallableTypeOf - | MiscSpecialForm::Top - | MiscSpecialForm::Bottom - | MiscSpecialForm::Concatenate - | MiscSpecialForm::TypeGuard - | MiscSpecialForm::TypedDict - | MiscSpecialForm::TypeIs - | MiscSpecialForm::NamedTuple - | MiscSpecialForm::TypingSelf => true, - - MiscSpecialForm::Generic - | MiscSpecialForm::Protocol - | MiscSpecialForm::TypeAlias => false, - - // TODO: seems incorrect? - MiscSpecialForm::Unpack => false, - }, - } - } - - pub(super) const fn kind(self) -> SpecialFormCategory { match self { - // See the `SpecialFormCategory` doc-comment for why these three are - // treated as their own category. - Self::Callable => SpecialFormCategory::Callable, - Self::Tuple => SpecialFormCategory::Tuple, - Self::Type => SpecialFormCategory::Type, - - // Type qualifiers - Self::Final => SpecialFormCategory::TypeQualifier(TypeQualifier::Final), - Self::ClassVar => SpecialFormCategory::TypeQualifier(TypeQualifier::ClassVar), - Self::ReadOnly => SpecialFormCategory::TypeQualifier(TypeQualifier::ReadOnly), - Self::Required => SpecialFormCategory::TypeQualifier(TypeQualifier::Required), - Self::NotRequired => SpecialFormCategory::TypeQualifier(TypeQualifier::NotRequired), - - // Legacy standard library aliases - Self::List => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::List), - Self::Dict => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Dict), - Self::Set => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Set), - Self::FrozenSet => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet), - Self::ChainMap => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap), - Self::Counter => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Counter), - Self::Deque => SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::Deque), - Self::DefaultDict => { - SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) - } - Self::OrderedDict => { - SpecialFormCategory::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) - } + Self::Type | Self::Tuple | Self::Callable | Self::LegacyStdlibAlias(_) => true, + Self::TypeQualifier(_) => false, + + Self::Any + | Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Never + | Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf + | Self::Top + | Self::Bottom + | Self::Concatenate + | Self::TypeGuard + | Self::TypedDict + | Self::TypeIs + | Self::NamedTuple + | Self::TypingSelf => true, - // Non-standard-library aliases - Self::AlwaysFalsy => SpecialFormCategory::Other(MiscSpecialForm::AlwaysFalsy), - Self::Unknown => SpecialFormCategory::Other(MiscSpecialForm::Unknown), - Self::Not => SpecialFormCategory::Other(MiscSpecialForm::Not), - Self::TypeOf => SpecialFormCategory::Other(MiscSpecialForm::TypeOf), - Self::Top => SpecialFormCategory::Other(MiscSpecialForm::Top), - Self::Bottom => SpecialFormCategory::Other(MiscSpecialForm::Bottom), - Self::Annotated => SpecialFormCategory::Other(MiscSpecialForm::Annotated), - Self::Any => SpecialFormCategory::Other(MiscSpecialForm::Any), - Self::Literal => SpecialFormCategory::Other(MiscSpecialForm::Literal), - Self::Optional => SpecialFormCategory::Other(MiscSpecialForm::Optional), - Self::Union => SpecialFormCategory::Other(MiscSpecialForm::Union), - Self::NoReturn => SpecialFormCategory::Other(MiscSpecialForm::NoReturn), - Self::Never => SpecialFormCategory::Other(MiscSpecialForm::Never), - Self::Concatenate => SpecialFormCategory::Other(MiscSpecialForm::Concatenate), - Self::Unpack => SpecialFormCategory::Other(MiscSpecialForm::Unpack), - Self::TypeAlias => SpecialFormCategory::Other(MiscSpecialForm::TypeAlias), - Self::TypeGuard => SpecialFormCategory::Other(MiscSpecialForm::TypeGuard), - Self::TypedDict => SpecialFormCategory::Other(MiscSpecialForm::TypedDict), - Self::TypeIs => SpecialFormCategory::Other(MiscSpecialForm::TypeIs), - Self::Protocol => SpecialFormCategory::Other(MiscSpecialForm::Protocol), - Self::Generic => SpecialFormCategory::Other(MiscSpecialForm::Generic), - Self::NamedTuple => SpecialFormCategory::Other(MiscSpecialForm::NamedTuple), - Self::AlwaysTruthy => SpecialFormCategory::Other(MiscSpecialForm::AlwaysTruthy), - Self::Intersection => SpecialFormCategory::Other(MiscSpecialForm::Intersection), - Self::TypingSelf => SpecialFormCategory::Other(MiscSpecialForm::TypingSelf), - Self::LiteralString => SpecialFormCategory::Other(MiscSpecialForm::LiteralString), - Self::CallableTypeOf => SpecialFormCategory::Other(MiscSpecialForm::CallableTypeOf), + Self::Generic | Self::Protocol | Self::TypeAlias => false, + + // TODO: seems incorrect? + Self::Unpack => false, } } /// Return `true` if this special form is valid as the second argument /// to `issubclass()` and `isinstance()` calls. pub(super) const fn is_valid_isinstance_target(self) -> bool { - match self.kind() { - SpecialFormCategory::LegacyStdlibAlias(_) - | SpecialFormCategory::Callable - | SpecialFormCategory::Tuple - | SpecialFormCategory::Type => true, - SpecialFormCategory::TypeQualifier(_) => false, - SpecialFormCategory::Other(form) => match form { - MiscSpecialForm::Protocol | MiscSpecialForm::Generic => true, - MiscSpecialForm::Any - | MiscSpecialForm::Annotated - | MiscSpecialForm::Literal - | MiscSpecialForm::LiteralString - | MiscSpecialForm::Optional - | MiscSpecialForm::Union - | MiscSpecialForm::NoReturn - | MiscSpecialForm::Never - | MiscSpecialForm::Unknown - | MiscSpecialForm::AlwaysTruthy - | MiscSpecialForm::AlwaysFalsy - | MiscSpecialForm::Not - | MiscSpecialForm::Intersection - | MiscSpecialForm::TypeOf - | MiscSpecialForm::CallableTypeOf - | MiscSpecialForm::Top - | MiscSpecialForm::Bottom - | MiscSpecialForm::TypingSelf - | MiscSpecialForm::Concatenate - | MiscSpecialForm::Unpack - | MiscSpecialForm::TypeAlias - | MiscSpecialForm::TypeGuard - | MiscSpecialForm::TypedDict - | MiscSpecialForm::TypeIs - | MiscSpecialForm::NamedTuple => false, - }, + match self { + Self::LegacyStdlibAlias(_) | Self::Callable | Self::Tuple | Self::Type => true, + Self::TypeQualifier(_) => false, + Self::Protocol | Self::Generic => true, + Self::Any + | Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Never + | Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf + | Self::Top + | Self::Bottom + | Self::TypingSelf + | Self::Concatenate + | Self::Unpack + | Self::TypeAlias + | Self::TypeGuard + | Self::TypedDict + | Self::TypeIs + | Self::NamedTuple => false, } } /// Return the name of the symbol at runtime pub(super) const fn name(self) -> &'static str { match self { - SpecialFormType::Any => "Any", - SpecialFormType::Annotated => "Annotated", - SpecialFormType::Literal => "Literal", - SpecialFormType::LiteralString => "LiteralString", - SpecialFormType::Optional => "Optional", - SpecialFormType::Union => "Union", - SpecialFormType::NoReturn => "NoReturn", - SpecialFormType::Never => "Never", - SpecialFormType::Tuple => "Tuple", - SpecialFormType::Type => "Type", - SpecialFormType::TypingSelf => "Self", - SpecialFormType::Final => "Final", - SpecialFormType::ClassVar => "ClassVar", - SpecialFormType::Callable => "Callable", - SpecialFormType::Concatenate => "Concatenate", - SpecialFormType::Unpack => "Unpack", - SpecialFormType::Required => "Required", - SpecialFormType::NotRequired => "NotRequired", - SpecialFormType::TypeAlias => "TypeAlias", - SpecialFormType::TypeGuard => "TypeGuard", - SpecialFormType::TypedDict => "TypedDict", - SpecialFormType::TypeIs => "TypeIs", - SpecialFormType::List => "List", - SpecialFormType::Dict => "Dict", - SpecialFormType::DefaultDict => "DefaultDict", - SpecialFormType::Set => "Set", - SpecialFormType::FrozenSet => "FrozenSet", - SpecialFormType::Counter => "Counter", - SpecialFormType::Deque => "Deque", - SpecialFormType::ChainMap => "ChainMap", - SpecialFormType::OrderedDict => "OrderedDict", - SpecialFormType::ReadOnly => "ReadOnly", - SpecialFormType::Unknown => "Unknown", - SpecialFormType::AlwaysTruthy => "AlwaysTruthy", - SpecialFormType::AlwaysFalsy => "AlwaysFalsy", - SpecialFormType::Not => "Not", - SpecialFormType::Intersection => "Intersection", - SpecialFormType::TypeOf => "TypeOf", - SpecialFormType::CallableTypeOf => "CallableTypeOf", - SpecialFormType::Top => "Top", - SpecialFormType::Bottom => "Bottom", - SpecialFormType::Protocol => "Protocol", - SpecialFormType::Generic => "Generic", - SpecialFormType::NamedTuple => "NamedTuple", + Self::Any => "Any", + Self::Annotated => "Annotated", + Self::Literal => "Literal", + Self::LiteralString => "LiteralString", + Self::Optional => "Optional", + Self::Union => "Union", + Self::NoReturn => "NoReturn", + Self::Never => "Never", + Self::Tuple => "Tuple", + Self::Type => "Type", + Self::TypingSelf => "Self", + Self::TypeQualifier(TypeQualifier::Final) => "Final", + Self::TypeQualifier(TypeQualifier::ClassVar) => "ClassVar", + Self::Callable => "Callable", + Self::Concatenate => "Concatenate", + Self::Unpack => "Unpack", + Self::TypeQualifier(TypeQualifier::Required) => "Required", + Self::TypeQualifier(TypeQualifier::NotRequired) => "NotRequired", + Self::TypeAlias => "TypeAlias", + Self::TypeGuard => "TypeGuard", + Self::TypedDict => "TypedDict", + Self::TypeIs => "TypeIs", + Self::LegacyStdlibAlias(LegacyStdlibAlias::List) => "List", + Self::LegacyStdlibAlias(LegacyStdlibAlias::Dict) => "Dict", + Self::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) => "DefaultDict", + Self::LegacyStdlibAlias(LegacyStdlibAlias::Set) => "Set", + Self::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet) => "FrozenSet", + Self::LegacyStdlibAlias(LegacyStdlibAlias::Counter) => "Counter", + Self::LegacyStdlibAlias(LegacyStdlibAlias::Deque) => "Deque", + Self::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap) => "ChainMap", + Self::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) => "OrderedDict", + Self::TypeQualifier(TypeQualifier::ReadOnly) => "ReadOnly", + Self::Unknown => "Unknown", + Self::AlwaysTruthy => "AlwaysTruthy", + Self::AlwaysFalsy => "AlwaysFalsy", + Self::Not => "Not", + Self::Intersection => "Intersection", + Self::TypeOf => "TypeOf", + Self::CallableTypeOf => "CallableTypeOf", + Self::Top => "Top", + Self::Bottom => "Bottom", + Self::Protocol => "Protocol", + Self::Generic => "Generic", + Self::NamedTuple => "NamedTuple", } } /// Return the module(s) in which this special form could be defined fn definition_modules(self) -> &'static [KnownModule] { match self { - SpecialFormType::Any - | SpecialFormType::Annotated - | SpecialFormType::Literal - | SpecialFormType::LiteralString - | SpecialFormType::Optional - | SpecialFormType::Union - | SpecialFormType::NoReturn - | SpecialFormType::Never - | SpecialFormType::Tuple - | SpecialFormType::Type - | SpecialFormType::TypingSelf - | SpecialFormType::Final - | SpecialFormType::ClassVar - | SpecialFormType::Callable - | SpecialFormType::Concatenate - | SpecialFormType::Unpack - | SpecialFormType::Required - | SpecialFormType::NotRequired - | SpecialFormType::TypeAlias - | SpecialFormType::TypeGuard - | SpecialFormType::TypedDict - | SpecialFormType::TypeIs - | SpecialFormType::ReadOnly - | SpecialFormType::Protocol - | SpecialFormType::Generic - | SpecialFormType::NamedTuple - | SpecialFormType::List - | SpecialFormType::Dict - | SpecialFormType::DefaultDict - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::Counter - | SpecialFormType::Deque - | SpecialFormType::ChainMap - | SpecialFormType::OrderedDict => &[KnownModule::Typing, KnownModule::TypingExtensions], - - SpecialFormType::Unknown - | SpecialFormType::AlwaysTruthy - | SpecialFormType::AlwaysFalsy - | SpecialFormType::Not - | SpecialFormType::Intersection - | SpecialFormType::TypeOf - | SpecialFormType::CallableTypeOf - | SpecialFormType::Top - | SpecialFormType::Bottom => &[KnownModule::TyExtensions], + Self::Any + | Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Never + | Self::Tuple + | Self::Type + | Self::TypingSelf + | Self::TypeQualifier(_) + | Self::Callable + | Self::Concatenate + | Self::Unpack + | Self::TypeAlias + | Self::TypeGuard + | Self::TypedDict + | Self::TypeIs + | Self::Protocol + | Self::Generic + | Self::NamedTuple + | Self::LegacyStdlibAlias(_) => &[KnownModule::Typing, KnownModule::TypingExtensions], + + Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf + | Self::Top + | Self::Bottom => &[KnownModule::TyExtensions], } } @@ -607,191 +538,11 @@ impl SpecialFormType { }) .map(TypeDefinition::SpecialForm) } -} - -impl std::fmt::Display for SpecialFormType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}.{}", - self.definition_modules()[0].as_str(), - self.name() - ) - } -} - -/// Various categories of special forms. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) enum SpecialFormCategory { - /// Special forms that are simple aliases to classes elsewhere in the standard library. - LegacyStdlibAlias(LegacyStdlibAlias), - - /// Special forms that are type qualifiers - TypeQualifier(TypeQualifier), - /// The special form `typing.Tuple`. + /// Interpret this special form as an unparameterized type in a type-expression context. /// - /// While this is technically an alias to `builtins.tuple`, it requires special handling - /// for type-expression parsing. - Tuple, - - /// The special form `typing.Type`. - /// - /// While this is technically an alias to `builtins.type`, it requires special handling - /// for type-expression parsing. - Type, - - /// The special form `Callable`. - /// - /// While `typing.Callable` aliases `collections.abc.Callable`, we view both objects - /// as inhabiting the same special form type internally. Moreover, `Callable` requires - /// special handling for both type-expression parsing and `isinstance`/`issubclass` - /// narrowing. - Callable, - - /// Everything else... - Other(MiscSpecialForm), -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) enum LegacyStdlibAlias { - List, - Dict, - Set, - FrozenSet, - ChainMap, - Counter, - DefaultDict, - Deque, - OrderedDict, -} - -impl LegacyStdlibAlias { - pub(super) const fn alias_spec(self) -> AliasSpec { - let (class, expected_argument_number) = match self { - LegacyStdlibAlias::List => (KnownClass::List, 1), - LegacyStdlibAlias::Dict => (KnownClass::Dict, 2), - LegacyStdlibAlias::Set => (KnownClass::Set, 1), - LegacyStdlibAlias::FrozenSet => (KnownClass::FrozenSet, 1), - LegacyStdlibAlias::ChainMap => (KnownClass::ChainMap, 2), - LegacyStdlibAlias::Counter => (KnownClass::Counter, 1), - LegacyStdlibAlias::DefaultDict => (KnownClass::DefaultDict, 2), - LegacyStdlibAlias::Deque => (KnownClass::Deque, 1), - LegacyStdlibAlias::OrderedDict => (KnownClass::OrderedDict, 2), - }; - - AliasSpec { - class, - expected_argument_number, - } - } - - pub(super) const fn aliased_class(self) -> KnownClass { - self.alias_spec().class - } -} - -impl From for SpecialFormType { - fn from(value: LegacyStdlibAlias) -> Self { - match value { - LegacyStdlibAlias::List => SpecialFormType::List, - LegacyStdlibAlias::Dict => SpecialFormType::Dict, - LegacyStdlibAlias::Set => SpecialFormType::Set, - LegacyStdlibAlias::FrozenSet => SpecialFormType::FrozenSet, - LegacyStdlibAlias::ChainMap => SpecialFormType::ChainMap, - LegacyStdlibAlias::Counter => SpecialFormType::Counter, - LegacyStdlibAlias::DefaultDict => SpecialFormType::DefaultDict, - LegacyStdlibAlias::Deque => SpecialFormType::Deque, - LegacyStdlibAlias::OrderedDict => SpecialFormType::OrderedDict, - } - } -} - -impl std::fmt::Display for LegacyStdlibAlias { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - SpecialFormType::from(*self).fmt(f) - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] -pub(super) enum TypeQualifier { - ReadOnly, - Final, - ClassVar, - Required, - NotRequired, -} - -impl From for SpecialFormType { - fn from(value: TypeQualifier) -> Self { - match value { - TypeQualifier::ReadOnly => SpecialFormType::ReadOnly, - TypeQualifier::Final => SpecialFormType::Final, - TypeQualifier::ClassVar => SpecialFormType::ClassVar, - TypeQualifier::Required => SpecialFormType::Required, - TypeQualifier::NotRequired => SpecialFormType::NotRequired, - } - } -} - -impl From for TypeQualifiers { - fn from(value: TypeQualifier) -> Self { - match value { - TypeQualifier::ReadOnly => TypeQualifiers::READ_ONLY, - TypeQualifier::Final => TypeQualifiers::FINAL, - TypeQualifier::ClassVar => TypeQualifiers::CLASS_VAR, - TypeQualifier::Required => TypeQualifiers::REQUIRED, - TypeQualifier::NotRequired => TypeQualifiers::NOT_REQUIRED, - } - } -} - -impl std::fmt::Display for TypeQualifier { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - SpecialFormType::from(*self).fmt(f) - } -} - -/// Information regarding the [`KnownClass`] a [`LegacyStdlibAlias`] refers to. -pub(super) struct AliasSpec { - pub(super) class: KnownClass, - pub(super) expected_argument_number: usize, -} - -/// Enumeration of special forms that are not aliases to classes or special constructs -/// elsewhere in the Python standard library. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) enum MiscSpecialForm { - Any, - Annotated, - Literal, - LiteralString, - Optional, - Union, - NoReturn, - Never, - Unknown, - AlwaysTruthy, - AlwaysFalsy, - Not, - Intersection, - TypeOf, - CallableTypeOf, - Top, - Bottom, - TypingSelf, - Concatenate, - Unpack, - TypeAlias, - TypeGuard, - TypedDict, - TypeIs, - Protocol, - Generic, - NamedTuple, -} - -impl MiscSpecialForm { + /// This is called for the "misc" special forms that are not aliases, type qualifiers, + /// `Tuple`, `Type`, or `Callable` (those are handled by their respective call sites). pub(super) fn in_type_expression<'db>( self, db: &'db dyn Db, @@ -825,7 +576,7 @@ impl MiscSpecialForm { return Err(InvalidTypeExpressionError { fallback_type: Type::unknown(), invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::InvalidType(self.into(), scope_id) + InvalidTypeExpression::InvalidType(Type::SpecialForm(self), scope_id) ], }); }; @@ -833,7 +584,7 @@ impl MiscSpecialForm { Ok( typing_self(db, scope_id, typevar_binding_context, class.into()) .map(Type::TypeVar) - .unwrap_or(self.into()), + .unwrap_or(Type::SpecialForm(self)), ) } // We ensure that `typing.TypeAlias` used in the expected position (annotating an @@ -850,7 +601,7 @@ impl MiscSpecialForm { Self::Literal | Self::Union | Self::Intersection => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::RequiresArguments(self.into()) + InvalidTypeExpression::RequiresArguments(self) ], fallback_type: Type::unknown(), }), @@ -874,63 +625,140 @@ impl MiscSpecialForm { | Self::Unpack | Self::CallableTypeOf => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::RequiresOneArgument(self.into()) + InvalidTypeExpression::RequiresOneArgument(self) ], fallback_type: Type::unknown(), }), Self::Annotated | Self::Concatenate => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::RequiresTwoArguments(self.into()) + InvalidTypeExpression::RequiresTwoArguments(self) ], fallback_type: Type::unknown(), }), + + // We treat `typing.Type` exactly the same as `builtins.type`: + SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), + SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), + SpecialFormType::Callable => Ok(Type::Callable(CallableType::unknown(db))), + SpecialFormType::LegacyStdlibAlias(alias) => Ok(alias.aliased_class().to_instance(db)), + SpecialFormType::TypeQualifier(qualifier) => { + let err = match qualifier { + TypeQualifier::Final | TypeQualifier::ClassVar => { + InvalidTypeExpression::TypeQualifier(qualifier) + } + TypeQualifier::ReadOnly + | TypeQualifier::NotRequired + | TypeQualifier::Required => { + InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) + } + }; + Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![err], + fallback_type: Type::unknown(), + }) + } } } } -impl From for SpecialFormType { - fn from(value: MiscSpecialForm) -> Self { - match value { - MiscSpecialForm::Any => SpecialFormType::Any, - MiscSpecialForm::Annotated => SpecialFormType::Annotated, - MiscSpecialForm::Literal => SpecialFormType::Literal, - MiscSpecialForm::LiteralString => SpecialFormType::LiteralString, - MiscSpecialForm::Optional => SpecialFormType::Optional, - MiscSpecialForm::Union => SpecialFormType::Union, - MiscSpecialForm::NoReturn => SpecialFormType::NoReturn, - MiscSpecialForm::Never => SpecialFormType::Never, - MiscSpecialForm::Unknown => SpecialFormType::Unknown, - MiscSpecialForm::AlwaysTruthy => SpecialFormType::AlwaysTruthy, - MiscSpecialForm::AlwaysFalsy => SpecialFormType::AlwaysFalsy, - MiscSpecialForm::Not => SpecialFormType::Not, - MiscSpecialForm::Intersection => SpecialFormType::Intersection, - MiscSpecialForm::TypeOf => SpecialFormType::TypeOf, - MiscSpecialForm::CallableTypeOf => SpecialFormType::CallableTypeOf, - MiscSpecialForm::Top => SpecialFormType::Top, - MiscSpecialForm::Bottom => SpecialFormType::Bottom, - MiscSpecialForm::TypingSelf => SpecialFormType::TypingSelf, - MiscSpecialForm::Concatenate => SpecialFormType::Concatenate, - MiscSpecialForm::Unpack => SpecialFormType::Unpack, - MiscSpecialForm::TypeAlias => SpecialFormType::TypeAlias, - MiscSpecialForm::TypeGuard => SpecialFormType::TypeGuard, - MiscSpecialForm::TypedDict => SpecialFormType::TypedDict, - MiscSpecialForm::TypeIs => SpecialFormType::TypeIs, - MiscSpecialForm::Protocol => SpecialFormType::Protocol, - MiscSpecialForm::Generic => SpecialFormType::Generic, - MiscSpecialForm::NamedTuple => SpecialFormType::NamedTuple, +impl std::fmt::Display for SpecialFormType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}.{}", + self.definition_modules()[0].as_str(), + self.name() + ) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +pub enum LegacyStdlibAlias { + List, + Dict, + Set, + FrozenSet, + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, +} + +impl LegacyStdlibAlias { + pub(super) const fn alias_spec(self) -> AliasSpec { + let (class, expected_argument_number) = match self { + LegacyStdlibAlias::List => (KnownClass::List, 1), + LegacyStdlibAlias::Dict => (KnownClass::Dict, 2), + LegacyStdlibAlias::Set => (KnownClass::Set, 1), + LegacyStdlibAlias::FrozenSet => (KnownClass::FrozenSet, 1), + LegacyStdlibAlias::ChainMap => (KnownClass::ChainMap, 2), + LegacyStdlibAlias::Counter => (KnownClass::Counter, 1), + LegacyStdlibAlias::DefaultDict => (KnownClass::DefaultDict, 2), + LegacyStdlibAlias::Deque => (KnownClass::Deque, 1), + LegacyStdlibAlias::OrderedDict => (KnownClass::OrderedDict, 2), + }; + + AliasSpec { + class, + expected_argument_number, } } + + pub(super) const fn aliased_class(self) -> KnownClass { + self.alias_spec().class + } +} + +impl From for SpecialFormType { + fn from(value: LegacyStdlibAlias) -> Self { + SpecialFormType::LegacyStdlibAlias(value) + } +} + +impl std::fmt::Display for LegacyStdlibAlias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + SpecialFormType::from(*self).fmt(f) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +pub enum TypeQualifier { + ReadOnly, + Final, + ClassVar, + Required, + NotRequired, +} + +impl From for SpecialFormType { + fn from(value: TypeQualifier) -> Self { + SpecialFormType::TypeQualifier(value) + } } -impl From for Type<'_> { - fn from(value: MiscSpecialForm) -> Self { - Type::SpecialForm(SpecialFormType::from(value)) +impl From for TypeQualifiers { + fn from(value: TypeQualifier) -> Self { + match value { + TypeQualifier::ReadOnly => TypeQualifiers::READ_ONLY, + TypeQualifier::Final => TypeQualifiers::FINAL, + TypeQualifier::ClassVar => TypeQualifiers::CLASS_VAR, + TypeQualifier::Required => TypeQualifiers::REQUIRED, + TypeQualifier::NotRequired => TypeQualifiers::NOT_REQUIRED, + } } } -impl std::fmt::Display for MiscSpecialForm { +impl std::fmt::Display for TypeQualifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { SpecialFormType::from(*self).fmt(f) } } + +/// Information regarding the [`KnownClass`] a [`LegacyStdlibAlias`] refers to. +#[derive(Debug)] +pub(super) struct AliasSpec { + pub(super) class: KnownClass, + pub(super) expected_argument_number: usize, +} From 252f8724cbf320bfd288e9e3032034577c5198c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 15:03:50 +0000 Subject: [PATCH 10/11] Reduce diff vs main: reorder match arms and use SpecialFormType:: prefix - Reorder branches in is_valid_in_type_expression to match main's structure (false arm first, true arm second) - Reorder branches in is_valid_isinstance_target to match main's structure (two arms: true first, false second) - Use SpecialFormType:: instead of Self:: in name() and definition_modules() match patterns to match main's style https://claude.ai/code/session_01AgLYCKoBuLEDsmkn1cnmvZ --- .../src/types/special_form.rs | 495 +++++++++++------- 1 file changed, 314 insertions(+), 181 deletions(-) diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 074f830bdee7d..48448fd5af8f8 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -7,13 +7,12 @@ use crate::semantic_index::place::ScopedPlaceId; use crate::semantic_index::{ FileScopeId, definition::Definition, place_table, scope::ScopeId, semantic_index, use_def_map, }; -use crate::types::TypeQualifiers; -use crate::types::{CallableType, TypeDefinition}; use crate::types::{ - IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, generics::typing_self, - infer::nearest_enclosing_class, + CallableType, IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, + TypeDefinition, TypeQualifiers, generics::typing_self, infer::nearest_enclosing_class, }; use ruff_db::files::File; +use strum_macros::EnumString; use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; /// Enumeration of specific runtime symbols that are special enough @@ -26,9 +25,7 @@ use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; /// # Ordering /// /// Ordering is stable and should be the same between runs. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, -)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] pub enum SpecialFormType { /// Special forms that are simple aliases to classes elsewhere in the standard library. LegacyStdlibAlias(LegacyStdlibAlias), @@ -193,53 +190,178 @@ impl SpecialFormType { /// Parse a `SpecialFormType` from its runtime symbol name. fn from_name(name: &str) -> Option { - Some(match name { - "Any" => Self::Any, - "Annotated" => Self::Annotated, - "Literal" => Self::Literal, - "LiteralString" => Self::LiteralString, - "Optional" => Self::Optional, - "Union" => Self::Union, - "NoReturn" => Self::NoReturn, - "Never" => Self::Never, - "Tuple" => Self::Tuple, - "Type" => Self::Type, - "Self" => Self::TypingSelf, - "Final" => Self::TypeQualifier(TypeQualifier::Final), - "ClassVar" => Self::TypeQualifier(TypeQualifier::ClassVar), - "Callable" => Self::Callable, - "Concatenate" => Self::Concatenate, - "Unpack" => Self::Unpack, - "Required" => Self::TypeQualifier(TypeQualifier::Required), - "NotRequired" => Self::TypeQualifier(TypeQualifier::NotRequired), - "TypeAlias" => Self::TypeAlias, - "TypeGuard" => Self::TypeGuard, - "TypedDict" => Self::TypedDict, - "TypeIs" => Self::TypeIs, - "ReadOnly" => Self::TypeQualifier(TypeQualifier::ReadOnly), - "List" => Self::LegacyStdlibAlias(LegacyStdlibAlias::List), - "Dict" => Self::LegacyStdlibAlias(LegacyStdlibAlias::Dict), - "DefaultDict" => Self::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict), - "Set" => Self::LegacyStdlibAlias(LegacyStdlibAlias::Set), - "FrozenSet" => Self::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet), - "Counter" => Self::LegacyStdlibAlias(LegacyStdlibAlias::Counter), - "Deque" => Self::LegacyStdlibAlias(LegacyStdlibAlias::Deque), - "ChainMap" => Self::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap), - "OrderedDict" => Self::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict), - "Unknown" => Self::Unknown, - "AlwaysTruthy" => Self::AlwaysTruthy, - "AlwaysFalsy" => Self::AlwaysFalsy, - "Not" => Self::Not, - "Intersection" => Self::Intersection, - "TypeOf" => Self::TypeOf, - "CallableTypeOf" => Self::CallableTypeOf, - "Top" => Self::Top, - "Bottom" => Self::Bottom, - "Protocol" => Self::Protocol, - "Generic" => Self::Generic, - "NamedTuple" => Self::NamedTuple, - _ => return None, - }) + /// An enum that maps 1:1 with `SpecialFormType`, but which holds no associated data + /// (and therefore can have `EnumString` derived on it). + /// This is much more robust than having a manual `from_string` method that matches + /// on string literals, because experience has shown it's very easy to forget to + /// update such a method when adding new variants. + #[derive(EnumString)] + enum SpecialFormTypeBuilder { + Tuple, + Type, + Callable, + Any, + Annotated, + Literal, + LiteralString, + Optional, + Union, + NoReturn, + Never, + Unknown, + AlwaysTruthy, + AlwaysFalsy, + Not, + Intersection, + TypeOf, + CallableTypeOf, + Top, + Bottom, + #[strum(serialize = "Self")] + TypingSelf, + Concatenate, + Unpack, + TypeAlias, + TypeGuard, + TypedDict, + TypeIs, + Protocol, + Generic, + NamedTuple, + List, + Dict, + FrozenSet, + Set, + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, + Final, + ClassVar, + ReadOnly, + Required, + NotRequired, + } + + // This implementation exists purely to enforce that every variant of `SpecialFormType` + // is included in the `SpecialFormTypeBuilder` enum + #[cfg(test)] + impl From for SpecialFormTypeBuilder { + fn from(value: SpecialFormType) -> Self { + match value { + SpecialFormType::AlwaysFalsy => Self::AlwaysFalsy, + SpecialFormType::AlwaysTruthy => Self::AlwaysTruthy, + SpecialFormType::Annotated => Self::Annotated, + SpecialFormType::Callable => Self::Callable, + SpecialFormType::CallableTypeOf => Self::CallableTypeOf, + SpecialFormType::Concatenate => Self::Concatenate, + SpecialFormType::Intersection => Self::Intersection, + SpecialFormType::Literal => Self::Literal, + SpecialFormType::LiteralString => Self::LiteralString, + SpecialFormType::Never => Self::Never, + SpecialFormType::NoReturn => Self::NoReturn, + SpecialFormType::Not => Self::Not, + SpecialFormType::Optional => Self::Optional, + SpecialFormType::Protocol => Self::Protocol, + SpecialFormType::Type => Self::Type, + SpecialFormType::TypeAlias => Self::TypeAlias, + SpecialFormType::TypeGuard => Self::TypeGuard, + SpecialFormType::TypeIs => Self::TypeIs, + SpecialFormType::TypingSelf => Self::TypingSelf, + SpecialFormType::Union => Self::Union, + SpecialFormType::Unknown => Self::Unknown, + SpecialFormType::Generic => Self::Generic, + SpecialFormType::NamedTuple => Self::NamedTuple, + SpecialFormType::Any => Self::Any, + SpecialFormType::Bottom => Self::Bottom, + SpecialFormType::Top => Self::Top, + SpecialFormType::Unpack => Self::Unpack, + SpecialFormType::Tuple => Self::Tuple, + SpecialFormType::TypedDict => Self::TypedDict, + SpecialFormType::TypeOf => Self::TypeOf, + SpecialFormType::LegacyStdlibAlias(alias) => match alias { + LegacyStdlibAlias::List => Self::List, + LegacyStdlibAlias::Dict => Self::Dict, + LegacyStdlibAlias::Set => Self::Set, + LegacyStdlibAlias::FrozenSet => Self::FrozenSet, + LegacyStdlibAlias::ChainMap => Self::ChainMap, + LegacyStdlibAlias::Counter => Self::Counter, + LegacyStdlibAlias::DefaultDict => Self::DefaultDict, + LegacyStdlibAlias::Deque => Self::Deque, + LegacyStdlibAlias::OrderedDict => Self::OrderedDict, + }, + SpecialFormType::TypeQualifier(qualifier) => match qualifier { + TypeQualifier::Final => Self::Final, + TypeQualifier::ClassVar => Self::ClassVar, + TypeQualifier::ReadOnly => Self::ReadOnly, + TypeQualifier::Required => Self::Required, + TypeQualifier::NotRequired => Self::NotRequired, + }, + } + } + } + + SpecialFormTypeBuilder::try_from(name) + .ok() + .map(|form| match form { + SpecialFormTypeBuilder::AlwaysFalsy => Self::AlwaysFalsy, + SpecialFormTypeBuilder::AlwaysTruthy => Self::AlwaysTruthy, + SpecialFormTypeBuilder::Annotated => Self::Annotated, + SpecialFormTypeBuilder::Callable => Self::Callable, + SpecialFormTypeBuilder::CallableTypeOf => Self::CallableTypeOf, + SpecialFormTypeBuilder::Concatenate => Self::Concatenate, + SpecialFormTypeBuilder::Intersection => Self::Intersection, + SpecialFormTypeBuilder::Literal => Self::Literal, + SpecialFormTypeBuilder::LiteralString => Self::LiteralString, + SpecialFormTypeBuilder::Never => Self::Never, + SpecialFormTypeBuilder::NoReturn => Self::NoReturn, + SpecialFormTypeBuilder::Not => Self::Not, + SpecialFormTypeBuilder::Optional => Self::Optional, + SpecialFormTypeBuilder::Protocol => Self::Protocol, + SpecialFormTypeBuilder::Type => Self::Type, + SpecialFormTypeBuilder::TypeAlias => Self::TypeAlias, + SpecialFormTypeBuilder::TypeGuard => Self::TypeGuard, + SpecialFormTypeBuilder::TypeIs => Self::TypeIs, + SpecialFormTypeBuilder::TypingSelf => Self::TypingSelf, + SpecialFormTypeBuilder::Union => Self::Union, + SpecialFormTypeBuilder::Unknown => Self::Unknown, + SpecialFormTypeBuilder::Generic => Self::Generic, + SpecialFormTypeBuilder::NamedTuple => Self::NamedTuple, + SpecialFormTypeBuilder::Any => Self::Any, + SpecialFormTypeBuilder::Bottom => Self::Bottom, + SpecialFormTypeBuilder::Top => Self::Top, + SpecialFormTypeBuilder::Unpack => Self::Unpack, + SpecialFormTypeBuilder::Tuple => Self::Tuple, + SpecialFormTypeBuilder::TypedDict => Self::TypedDict, + SpecialFormTypeBuilder::TypeOf => Self::TypeOf, + SpecialFormTypeBuilder::List => Self::LegacyStdlibAlias(LegacyStdlibAlias::List), + SpecialFormTypeBuilder::Dict => Self::LegacyStdlibAlias(LegacyStdlibAlias::Dict), + SpecialFormTypeBuilder::Set => Self::LegacyStdlibAlias(LegacyStdlibAlias::Set), + SpecialFormTypeBuilder::FrozenSet => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet) + } + SpecialFormTypeBuilder::ChainMap => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap) + } + SpecialFormTypeBuilder::Counter => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::Counter) + } + SpecialFormTypeBuilder::DefaultDict => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) + } + SpecialFormTypeBuilder::Deque => Self::LegacyStdlibAlias(LegacyStdlibAlias::Deque), + SpecialFormTypeBuilder::OrderedDict => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) + } + SpecialFormTypeBuilder::Final => Self::TypeQualifier(TypeQualifier::Final), + SpecialFormTypeBuilder::ClassVar => Self::TypeQualifier(TypeQualifier::ClassVar), + SpecialFormTypeBuilder::ReadOnly => Self::TypeQualifier(TypeQualifier::ReadOnly), + SpecialFormTypeBuilder::Required => Self::TypeQualifier(TypeQualifier::Required), + SpecialFormTypeBuilder::NotRequired => { + Self::TypeQualifier(TypeQualifier::NotRequired) + } + }) } /// Return `true` if `module` is a module from which this `SpecialFormType` variant can validly originate. @@ -364,37 +486,41 @@ impl SpecialFormType { /// pub(super) const fn is_valid_in_type_expression(self) -> bool { match self { - Self::Type | Self::Tuple | Self::Callable | Self::LegacyStdlibAlias(_) => true, - Self::TypeQualifier(_) => false, - - Self::Any - | Self::Annotated - | Self::Literal - | Self::LiteralString - | Self::Optional - | Self::Union - | Self::NoReturn - | Self::Never - | Self::Unknown - | Self::AlwaysTruthy - | Self::AlwaysFalsy - | Self::Not - | Self::Intersection - | Self::TypeOf - | Self::CallableTypeOf - | Self::Top - | Self::Bottom - | Self::Concatenate - | Self::TypeGuard - | Self::TypedDict - | Self::TypeIs - | Self::NamedTuple - | Self::TypingSelf => true, - - Self::Generic | Self::Protocol | Self::TypeAlias => false, - - // TODO: seems incorrect? - Self::Unpack => false, + SpecialFormType::Annotated + | SpecialFormType::Any + | SpecialFormType::Literal + | SpecialFormType::LiteralString + | SpecialFormType::Optional + | SpecialFormType::Union + | SpecialFormType::NoReturn + | SpecialFormType::Never + | SpecialFormType::Tuple + | SpecialFormType::LegacyStdlibAlias(_) + | SpecialFormType::Type + | SpecialFormType::Unknown + | SpecialFormType::AlwaysTruthy + | SpecialFormType::AlwaysFalsy + | SpecialFormType::Not + | SpecialFormType::Intersection + | SpecialFormType::TypeOf + | SpecialFormType::CallableTypeOf + | SpecialFormType::Top + | SpecialFormType::Bottom + | SpecialFormType::Callable + | SpecialFormType::TypingSelf + | SpecialFormType::Concatenate + | SpecialFormType::TypeGuard + | SpecialFormType::TypedDict + | SpecialFormType::TypeIs + | SpecialFormType::NamedTuple => true, + + SpecialFormType::TypeQualifier(_) + | SpecialFormType::TypeAlias + | SpecialFormType::Protocol + | SpecialFormType::Generic => false, + + // TODO -- seems incorrect? + SpecialFormType::Unpack => false, } } @@ -402,123 +528,130 @@ impl SpecialFormType { /// to `issubclass()` and `isinstance()` calls. pub(super) const fn is_valid_isinstance_target(self) -> bool { match self { - Self::LegacyStdlibAlias(_) | Self::Callable | Self::Tuple | Self::Type => true, - Self::TypeQualifier(_) => false, - Self::Protocol | Self::Generic => true, - Self::Any + Self::Callable + | Self::LegacyStdlibAlias(_) + | Self::Tuple + | Self::Type + | Self::Protocol + | Self::Generic => true, + + Self::AlwaysFalsy + | Self::AlwaysTruthy | Self::Annotated + | Self::Bottom + | Self::CallableTypeOf + | Self::TypeQualifier(_) + | Self::Concatenate + | Self::Intersection | Self::Literal | Self::LiteralString - | Self::Optional - | Self::Union - | Self::NoReturn | Self::Never - | Self::Unknown - | Self::AlwaysTruthy - | Self::AlwaysFalsy + | Self::NoReturn | Self::Not - | Self::Intersection - | Self::TypeOf - | Self::CallableTypeOf - | Self::Top - | Self::Bottom - | Self::TypingSelf - | Self::Concatenate - | Self::Unpack | Self::TypeAlias | Self::TypeGuard - | Self::TypedDict + | Self::NamedTuple + | Self::Optional + | Self::Top | Self::TypeIs - | Self::NamedTuple => false, + | Self::TypedDict + | Self::TypingSelf + | Self::Union + | Self::Unknown + | Self::TypeOf + | Self::Any // can be used in `issubclass()` but not `isinstance()`. + | Self::Unpack => false, } } /// Return the name of the symbol at runtime pub(super) const fn name(self) -> &'static str { match self { - Self::Any => "Any", - Self::Annotated => "Annotated", - Self::Literal => "Literal", - Self::LiteralString => "LiteralString", - Self::Optional => "Optional", - Self::Union => "Union", - Self::NoReturn => "NoReturn", - Self::Never => "Never", - Self::Tuple => "Tuple", - Self::Type => "Type", - Self::TypingSelf => "Self", - Self::TypeQualifier(TypeQualifier::Final) => "Final", - Self::TypeQualifier(TypeQualifier::ClassVar) => "ClassVar", - Self::Callable => "Callable", - Self::Concatenate => "Concatenate", - Self::Unpack => "Unpack", - Self::TypeQualifier(TypeQualifier::Required) => "Required", - Self::TypeQualifier(TypeQualifier::NotRequired) => "NotRequired", - Self::TypeAlias => "TypeAlias", - Self::TypeGuard => "TypeGuard", - Self::TypedDict => "TypedDict", - Self::TypeIs => "TypeIs", - Self::LegacyStdlibAlias(LegacyStdlibAlias::List) => "List", - Self::LegacyStdlibAlias(LegacyStdlibAlias::Dict) => "Dict", - Self::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) => "DefaultDict", - Self::LegacyStdlibAlias(LegacyStdlibAlias::Set) => "Set", - Self::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet) => "FrozenSet", - Self::LegacyStdlibAlias(LegacyStdlibAlias::Counter) => "Counter", - Self::LegacyStdlibAlias(LegacyStdlibAlias::Deque) => "Deque", - Self::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap) => "ChainMap", - Self::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) => "OrderedDict", - Self::TypeQualifier(TypeQualifier::ReadOnly) => "ReadOnly", - Self::Unknown => "Unknown", - Self::AlwaysTruthy => "AlwaysTruthy", - Self::AlwaysFalsy => "AlwaysFalsy", - Self::Not => "Not", - Self::Intersection => "Intersection", - Self::TypeOf => "TypeOf", - Self::CallableTypeOf => "CallableTypeOf", - Self::Top => "Top", - Self::Bottom => "Bottom", - Self::Protocol => "Protocol", - Self::Generic => "Generic", - Self::NamedTuple => "NamedTuple", + SpecialFormType::Any => "Any", + SpecialFormType::Annotated => "Annotated", + SpecialFormType::Literal => "Literal", + SpecialFormType::LiteralString => "LiteralString", + SpecialFormType::Optional => "Optional", + SpecialFormType::Union => "Union", + SpecialFormType::NoReturn => "NoReturn", + SpecialFormType::Never => "Never", + SpecialFormType::Tuple => "Tuple", + SpecialFormType::Type => "Type", + SpecialFormType::TypingSelf => "Self", + SpecialFormType::TypeQualifier(TypeQualifier::Final) => "Final", + SpecialFormType::TypeQualifier(TypeQualifier::ClassVar) => "ClassVar", + SpecialFormType::Callable => "Callable", + SpecialFormType::Concatenate => "Concatenate", + SpecialFormType::Unpack => "Unpack", + SpecialFormType::TypeQualifier(TypeQualifier::Required) => "Required", + SpecialFormType::TypeQualifier(TypeQualifier::NotRequired) => "NotRequired", + SpecialFormType::TypeAlias => "TypeAlias", + SpecialFormType::TypeGuard => "TypeGuard", + SpecialFormType::TypedDict => "TypedDict", + SpecialFormType::TypeIs => "TypeIs", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::List) => "List", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Dict) => "Dict", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) => "DefaultDict", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Set) => "Set", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet) => "FrozenSet", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Counter) => "Counter", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Deque) => "Deque", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap) => "ChainMap", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) => "OrderedDict", + SpecialFormType::TypeQualifier(TypeQualifier::ReadOnly) => "ReadOnly", + SpecialFormType::Unknown => "Unknown", + SpecialFormType::AlwaysTruthy => "AlwaysTruthy", + SpecialFormType::AlwaysFalsy => "AlwaysFalsy", + SpecialFormType::Not => "Not", + SpecialFormType::Intersection => "Intersection", + SpecialFormType::TypeOf => "TypeOf", + SpecialFormType::CallableTypeOf => "CallableTypeOf", + SpecialFormType::Top => "Top", + SpecialFormType::Bottom => "Bottom", + SpecialFormType::Protocol => "Protocol", + SpecialFormType::Generic => "Generic", + SpecialFormType::NamedTuple => "NamedTuple", } } /// Return the module(s) in which this special form could be defined fn definition_modules(self) -> &'static [KnownModule] { match self { - Self::Any - | Self::Annotated - | Self::Literal - | Self::LiteralString - | Self::Optional - | Self::Union - | Self::NoReturn - | Self::Never - | Self::Tuple - | Self::Type - | Self::TypingSelf - | Self::TypeQualifier(_) - | Self::Callable - | Self::Concatenate - | Self::Unpack - | Self::TypeAlias - | Self::TypeGuard - | Self::TypedDict - | Self::TypeIs - | Self::Protocol - | Self::Generic - | Self::NamedTuple - | Self::LegacyStdlibAlias(_) => &[KnownModule::Typing, KnownModule::TypingExtensions], + SpecialFormType::Any + | SpecialFormType::Annotated + | SpecialFormType::Literal + | SpecialFormType::LiteralString + | SpecialFormType::Optional + | SpecialFormType::Union + | SpecialFormType::NoReturn + | SpecialFormType::Never + | SpecialFormType::Tuple + | SpecialFormType::Type + | SpecialFormType::TypingSelf + | SpecialFormType::TypeQualifier(_) + | SpecialFormType::Callable + | SpecialFormType::Concatenate + | SpecialFormType::Unpack + | SpecialFormType::TypeAlias + | SpecialFormType::TypeGuard + | SpecialFormType::TypedDict + | SpecialFormType::TypeIs + | SpecialFormType::Protocol + | SpecialFormType::Generic + | SpecialFormType::NamedTuple + | SpecialFormType::LegacyStdlibAlias(_) => { + &[KnownModule::Typing, KnownModule::TypingExtensions] + } - Self::Unknown - | Self::AlwaysTruthy - | Self::AlwaysFalsy - | Self::Not - | Self::Intersection - | Self::TypeOf - | Self::CallableTypeOf - | Self::Top - | Self::Bottom => &[KnownModule::TyExtensions], + SpecialFormType::Unknown + | SpecialFormType::AlwaysTruthy + | SpecialFormType::AlwaysFalsy + | SpecialFormType::Not + | SpecialFormType::Intersection + | SpecialFormType::TypeOf + | SpecialFormType::CallableTypeOf + | SpecialFormType::Top + | SpecialFormType::Bottom => &[KnownModule::TyExtensions], } } From befefc3ccefe7e6d110ab7db0031c2072d28e76d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 24 Feb 2026 11:51:18 +0000 Subject: [PATCH 11/11] fix bug where a type alias with `Unpack` at the top level caused us to emit a diagnostic saying that "type qualifiers are not allowed in type aliases" --- .../resources/mdtest/pep613_type_aliases.md | 7 ++- .../src/types/infer/builder.rs | 8 ++-- .../src/types/special_form.rs | 44 ------------------- 3 files changed, 9 insertions(+), 50 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index ad901d4042c28..8b1bb49e30d2d 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -478,9 +478,14 @@ bad4: TypeAlias = Final # error: [invalid-type-form] bad5: TypeAlias = Required[int] # error: [invalid-type-form] bad6: TypeAlias = NotRequired[int] # error: [invalid-type-form] bad7: TypeAlias = ReadOnly[int] # error: [invalid-type-form] -bad8: TypeAlias = Unpack[tuple[int, ...]] # error: [invalid-type-form] bad9: TypeAlias = InitVar[int] # error: [invalid-type-form] bad10: TypeAlias = InitVar # error: [invalid-type-form] + +# TODO: this should cause us to emit an error (`Unpack` is not valid at the +# top level in this context), but for different reasons to the above cases: +# `Unpack` is not a type qualifier, and so the error message in our diagnostic +# shouldn't say that it is. +differently_bad: TypeAlias = Unpack[tuple[int, ...]] ``` [type expression]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 41a4db1fd09a7..818ebc240fd55 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -9161,11 +9161,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if is_pep_613_type_alias { let is_valid_special_form = |ty: Type<'db>| match ty { - Type::SpecialForm(special_form) => special_form.is_valid_in_type_expression(), - Type::ClassLiteral(literal) - if literal.is_known(self.db(), KnownClass::InitVar) => - { - false + Type::SpecialForm(SpecialFormType::TypeQualifier(_)) => false, + Type::ClassLiteral(literal) => { + !literal.is_known(self.db(), KnownClass::InitVar) } _ => true, }; diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 48448fd5af8f8..b5c89466069e4 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -480,50 +480,6 @@ impl SpecialFormType { } } - /// Return `true` if this special form type is valid in a type-expression context (and not - /// just in an *annotation* expression context). See the following section of the typing - /// specification for more details: - /// - pub(super) const fn is_valid_in_type_expression(self) -> bool { - match self { - SpecialFormType::Annotated - | SpecialFormType::Any - | SpecialFormType::Literal - | SpecialFormType::LiteralString - | SpecialFormType::Optional - | SpecialFormType::Union - | SpecialFormType::NoReturn - | SpecialFormType::Never - | SpecialFormType::Tuple - | SpecialFormType::LegacyStdlibAlias(_) - | SpecialFormType::Type - | SpecialFormType::Unknown - | SpecialFormType::AlwaysTruthy - | SpecialFormType::AlwaysFalsy - | SpecialFormType::Not - | SpecialFormType::Intersection - | SpecialFormType::TypeOf - | SpecialFormType::CallableTypeOf - | SpecialFormType::Top - | SpecialFormType::Bottom - | SpecialFormType::Callable - | SpecialFormType::TypingSelf - | SpecialFormType::Concatenate - | SpecialFormType::TypeGuard - | SpecialFormType::TypedDict - | SpecialFormType::TypeIs - | SpecialFormType::NamedTuple => true, - - SpecialFormType::TypeQualifier(_) - | SpecialFormType::TypeAlias - | SpecialFormType::Protocol - | SpecialFormType::Generic => false, - - // TODO -- seems incorrect? - SpecialFormType::Unpack => false, - } - } - /// Return `true` if this special form is valid as the second argument /// to `issubclass()` and `isinstance()` calls. pub(super) const fn is_valid_isinstance_target(self) -> bool {