diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index 03e007681916d..16acbce4ded4c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -5,8 +5,7 @@ python-version = "3.12" ``` -Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing -spec. +Most of these tests come from the [Scoping rules for type variables] section of the typing spec. ## Typevar used outside of generic function or class @@ -410,6 +409,135 @@ class C[T]: ok2: Inner[T] ``` +## Type parameter defaults cannot reference outer-scope type parameters + +```toml +[environment] +python-version = "3.13" +``` + +Per the [typing spec][scoping rules], the default of a type parameter must not reference type +parameters from an outer scope. Out-of-scope defaults on class type parameters are validated as part +of `invalid-generic-class`; the tests here cover the remaining cases for PEP 695 function and type +alias scopes, as well as legacy `TypeVar`s used in function/method signatures. + +### Nested functions + + + +```py +def outer[T](): + # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" + def inner[U = T](): ... + def ok[U = int](): ... # OK +``` + +### Function nested in class + + + +```py +class C[T]: + # error: [invalid-type-variable-default] + def f[U = T](self): ... + def g[U = int](self): ... # OK +``` + +### Type alias nested in class + + + +```py +class C[T]: + # error: [invalid-type-variable-default] + type Alias[U = T] = list[U] + + type Ok[U = int] = list[U] # OK +``` + +### Legacy TypeVar in method with outer-scope class TypeVar + + + +```py +from typing import TypeVar, Generic + +T1 = TypeVar("T1") +T2 = TypeVar("T2", default=T1) + +class Foo(Generic[T1]): + # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `T1`" + def method(self, x: T2) -> T2: + return x +``` + +### Legacy TypeVar in nested function + + + +```py +from typing import TypeVar, Generic + +T = TypeVar("T") +U = TypeVar("U", default=T) + +def outer(x: T) -> T: + # error: [invalid-type-variable-default] + def inner(y: U) -> U: + return y + return x +``` + +### Legacy TypeVar with default referring to later Typevar + + + +```py +from typing import TypeVar, Generic + +T = TypeVar("T", default=int) +U = TypeVar("U", default=T) + +# error: [invalid-type-variable-default] +def bad(y: U, z: T) -> tuple[U, T]: + return y, z + +# OK, because the typevar with the default comes after the one without +def fine(y: T, z: U) -> tuple[U, T]: + return z, y +``` + +### Legacy TypeVar ordering: default before non-default in function + + + +```py +from typing import TypeVar + +T1 = TypeVar("T1", default=int) +T2 = TypeVar("T2") +T3 = TypeVar("T3") +DefaultStrT = TypeVar("DefaultStrT", default=str) + +# error: [invalid-type-variable-default] +def f(x: T1, y: T2) -> tuple[T1, T2]: + return x, y + +# error: [invalid-type-variable-default] +def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: + return x, y, z + +# error: [invalid-type-variable-default] +def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: + return x, y, z, w + +def ok(x: T2, y: T1) -> tuple[T2, T1]: + return x, y + +def ok2(x: T1, y: DefaultStrT) -> tuple[T1, DefaultStrT]: + return x, y +``` + ## Mixed-scope type parameters Methods can have type parameters that are scoped to the method itself, while also referring to type @@ -433,4 +561,5 @@ def f(x: type[Foo[T]]) -> T: raise NotImplementedError ``` -[scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables +[scoping rules]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules +[scoping rules for type variables]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" new file mode 100644 index 0000000000000..bf806c00c1af7 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" @@ -0,0 +1,39 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 624 +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Function nested in class +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C[T]: +2 | # error: [invalid-type-variable-default] +3 | def f[U = T](self): ... +4 | def g[U = int](self): ... # OK +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid default for type parameter `U` + --> src/mdtest_snippet.py:1:9 + | +1 | class C[T]: + | - `T` defined here +2 | # error: [invalid-type-variable-default] +3 | def f[U = T](self): ... + | ^ `T` is a type parameter bound in an outer scope +4 | def g[U = int](self): ... # OK + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" new file mode 100644 index 0000000000000..ba4ce7ed1fb27 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" @@ -0,0 +1,46 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar in method with outer-scope class TypeVar +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar, Generic +2 | +3 | T1 = TypeVar("T1") +4 | T2 = TypeVar("T2", default=T1) +5 | +6 | class Foo(Generic[T1]): +7 | # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `T1`" +8 | def method(self, x: T2) -> T2: +9 | return x +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid use of type variable `T2` + --> src/mdtest_snippet.py:4:1 + | +3 | T1 = TypeVar("T1") +4 | T2 = TypeVar("T2", default=T1) + | ------------------------------ `T2` defined here +5 | +6 | class Foo(Generic[T1]): +7 | # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable … +8 | def method(self, x: T2) -> T2: + | ^^ Default of `T2` references out-of-scope type variable `T1` +9 | return x + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" new file mode 100644 index 0000000000000..cf21e648fa8cd --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" @@ -0,0 +1,49 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 624 +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar in nested function +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar, Generic + 2 | + 3 | T = TypeVar("T") + 4 | U = TypeVar("U", default=T) + 5 | + 6 | def outer(x: T) -> T: + 7 | # error: [invalid-type-variable-default] + 8 | def inner(y: U) -> U: + 9 | return y +10 | return x +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid use of type variable `U` + --> src/mdtest_snippet.py:4:1 + | + 3 | T = TypeVar("T") + 4 | U = TypeVar("U", default=T) + | --------------------------- `U` defined here + 5 | + 6 | def outer(x: T) -> T: + 7 | # error: [invalid-type-variable-default] + 8 | def inner(y: U) -> U: + | ^ Default of `U` references out-of-scope type variable `T` + 9 | return y +10 | return x + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" new file mode 100644 index 0000000000000..dd6d4aab02275 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" @@ -0,0 +1,120 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar ordering: default before non-default in function +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar + 2 | + 3 | T1 = TypeVar("T1", default=int) + 4 | T2 = TypeVar("T2") + 5 | T3 = TypeVar("T3") + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + 7 | + 8 | # error: [invalid-type-variable-default] + 9 | def f(x: T1, y: T2) -> tuple[T1, T2]: +10 | return x, y +11 | +12 | # error: [invalid-type-variable-default] +13 | def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: +14 | return x, y, z +15 | +16 | # error: [invalid-type-variable-default] +17 | def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: +18 | return x, y, z, w +19 | +20 | def ok(x: T2, y: T1) -> tuple[T2, T1]: +21 | return x, y +22 | +23 | def ok2(x: T1, y: DefaultStrT) -> tuple[T1, DefaultStrT]: +24 | return x, y +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:9:10 + | + 8 | # error: [invalid-type-variable-default] + 9 | def f(x: T1, y: T2) -> tuple[T1, T2]: + | -- ^^ Type variable `T2` does not have a default + | | + | Earlier TypeVar `T1` has a default +10 | return x, y + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | T2 = TypeVar("T2") + | ------------------ `T2` defined here + 5 | T3 = TypeVar("T3") + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + | +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:13:17 + | +12 | # error: [invalid-type-variable-default] +13 | def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: + | -- ^^ Type variable `T3` does not have a default + | | + | Earlier TypeVar `T1` has a default +14 | return x, y, z + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | T2 = TypeVar("T2") + 5 | T3 = TypeVar("T3") + | ------------------ `T3` defined here + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + | +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:17:10 + | +16 | # error: [invalid-type-variable-default] +17 | def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: + | -- ^^ Type variables `T2` and `T3` do not have defaults + | | + | Earlier TypeVar `T1` has a default +18 | return x, y, z, w + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | T2 = TypeVar("T2") + | ------------------ `T2` defined here + 5 | T3 = TypeVar("T3") + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + | +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" new file mode 100644 index 0000000000000..87d020644136f --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" @@ -0,0 +1,48 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar with default referring to later Typevar +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar, Generic + 2 | + 3 | T = TypeVar("T", default=int) + 4 | U = TypeVar("U", default=T) + 5 | + 6 | # error: [invalid-type-variable-default] + 7 | def bad(y: U, z: T) -> tuple[U, T]: + 8 | return y, z + 9 | +10 | # OK, because the typevar with the default comes after the one without +11 | def fine(y: T, z: U) -> tuple[U, T]: +12 | return z, y +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid use of type variable `U` + --> src/mdtest_snippet.py:4:1 + | +3 | T = TypeVar("T", default=int) +4 | U = TypeVar("U", default=T) + | --------------------------- `U` defined here +5 | +6 | # error: [invalid-type-variable-default] +7 | def bad(y: U, z: T) -> tuple[U, T]: + | ^ Default of `U` references later type parameter `T` +8 | return y, z + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" new file mode 100644 index 0000000000000..418057f234a37 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" @@ -0,0 +1,38 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Nested functions +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def outer[T](): +2 | # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" +3 | def inner[U = T](): ... +4 | def ok[U = int](): ... # OK +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid default for type parameter `U` + --> src/mdtest_snippet.py:1:11 + | +1 | def outer[T](): + | - `T` defined here +2 | # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" +3 | def inner[U = T](): ... + | ^ `T` is a type parameter bound in an outer scope +4 | def ok[U = int](): ... # OK + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" new file mode 100644 index 0000000000000..3ebc3bab85c36 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 624 +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Type alias nested in class +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C[T]: +2 | # error: [invalid-type-variable-default] +3 | type Alias[U = T] = list[U] +4 | +5 | type Ok[U = int] = list[U] # OK +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid default for type parameter `U` + --> src/mdtest_snippet.py:1:9 + | +1 | class C[T]: + | - `T` defined here +2 | # error: [invalid-type-variable-default] +3 | type Alias[U = T] = list[U] + | ^ `T` is a type parameter bound in an outer scope +4 | +5 | type Ok[U = int] = list[U] # OK + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 12305c81b42ef..862f68851e913 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -59,6 +59,7 @@ use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, attribute_assignments, place_table, }; +use crate::types::BindingContext; use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::callable::CallableTypeKind; @@ -689,21 +690,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.check_dynamic_class_definitions(&deferred_definitions); self.check_overloaded_functions(node); self.check_type_guard_definitions(); - self.check_legacy_positional_only_convention(); + self.check_function_definitions(); self.check_final_without_value(); } } - /// Iterate over all function definitions in this scope and check for invalid applications - /// of the pre-PEP-570 positional-only parameter convention. - fn check_legacy_positional_only_convention(&mut self) { + /// Iterate over all function definitions in this scope and run checks that + /// require access to the function's inferred signature (and therefore must + /// run after deferred inference is complete). + fn check_function_definitions(&self) { let db = self.db(); for (definition, _) in &self.declarations { if !definition.kind(db).is_function_def() { continue; } - let Some(Type::FunctionLiteral(function_type)) = infer_definition_types(db, *definition).undecorated_type() else { @@ -711,56 +712,313 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let last_definition = function_type.literal(db).last_definition(db); - let node = last_definition.node(db, self.file(), self.module()); + let signature = last_definition.raw_signature(db); - let ast_parameters = &node.parameters; - // If the function has any PEP-570 positional-only parameters, - // assume that `__`-prefixed parameters are not meant to be positional-only - if !ast_parameters.posonlyargs.is_empty() { + self.check_legacy_positional_only_convention(last_definition, &signature); + self.check_legacy_typevar_defaults(last_definition, &signature); + self.check_legacy_typevar_ordering(last_definition, &signature); + } + } + + /// Check for invalid applications of the pre-PEP-570 positional-only parameter convention. + fn check_legacy_positional_only_convention( + &self, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, + ) { + let node = last_definition.node(self.db(), self.file(), self.module()); + let ast_parameters = &node.parameters; + + // If the function has any PEP-570 positional-only parameters, + // assume that `__`-prefixed parameters are not meant to be positional-only + if !ast_parameters.posonlyargs.is_empty() { + return; + } + let parsed_parameters = signature.parameters(); + let mut previous_non_positional_only: Option<&ast::ParameterWithDefault> = None; + + for (param_node, param) in std::iter::zip(ast_parameters, parsed_parameters) { + let AnyParameterRef::NonVariadic(param_node) = param_node else { + continue; + }; + if param.is_positional_only() { continue; } - let signature = last_definition.raw_signature(db); - let parsed_parameters = signature.parameters(); - let mut previous_non_positional_only: Option<&ast::ParameterWithDefault> = None; - for (param_node, param) in std::iter::zip(ast_parameters, parsed_parameters) { - let AnyParameterRef::NonVariadic(param_node) = param_node else { + // Valid uses of the PEP-484 positional-only convention will have been detected as such + // in the first iteration over this scope, so `param.is_positional_only()` will return `true` + // for those. We only get here for invalid uses of the PEP-484 positional-only convention. + if param_node.uses_pep_484_positional_only_convention() { + let Some(builder) = self + .context + .report_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER, param_node.name()) + else { continue; }; - if param.is_positional_only() { - continue; + let mut diagnostic = builder.into_diagnostic( + "Invalid use of the legacy convention \ + for positional-only parameters", + ); + diagnostic.set_primary_message( + "Parameter name begins with `__` but will not be treated as positional-only", + ); + diagnostic.info( + "A parameter can only be positional-only \ + if it precedes all positional-or-keyword parameters", + ); + if let Some(earlier_node) = previous_non_positional_only { + diagnostic.annotate( + self.context + .secondary(earlier_node.name()) + .message("Prior parameter here was positional-or-keyword"), + ); } + } else if previous_non_positional_only.is_none() { + previous_non_positional_only = Some(param_node); + } + } + } - if param_node.uses_pep_484_positional_only_convention() { - if let Some(builder) = self - .context - .report_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER, param_node.name()) - { - let mut diagnostic = builder.into_diagnostic( - "Invalid use of the legacy convention \ - for positional-only parameters", - ); - diagnostic.set_primary_message( - "Parameter name begins with `__` \ - but will not be treated as positional-only", - ); - diagnostic.info( - "A parameter can only be positional-only \ - if it precedes all positional-or-keyword parameters", - ); - if let Some(earlier_node) = previous_non_positional_only { - diagnostic.annotate( - self.context - .secondary(earlier_node.name()) - .message("Prior parameter here was positional-or-keyword"), - ); - } - } - } else if previous_non_positional_only.is_none() { - previous_non_positional_only = Some(param_node); + /// Find the range of the first parameter annotation (or return type) in a function + /// whose inferred type references the given `TypeVar`, falling back to the function name. + fn find_typevar_annotation_range( + &self, + node: &ast::StmtFunctionDef, + typevar: TypeVarInstance<'db>, + ) -> TextRange { + let db = self.db(); + let typevar_id = typevar.identity(self.db()); + + node.parameters + .iter() + .filter_map(ast::AnyParameterRef::annotation) + .chain(node.returns.as_deref()) + .find(|ann| { + self.file_expression_type(ann) + .references_typevar(db, typevar_id) + }) + .map(Ranged::range) + .unwrap_or(node.name.range()) + } + + /// Check whether any legacy `TypeVar` used in a function signature has a default + /// that references an out-of-scope type variable. + /// + /// This check mirrors the class-level check at `report_invalid_typevar_default_reference`, + /// but for function/method generic contexts. + fn check_legacy_typevar_defaults( + &self, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, + ) { + let db = self.db(); + + let Some(generic_context) = signature.generic_context else { + return; + }; + + let typevars = generic_context + .variables(db) + .map(|bound_tvar| bound_tvar.typevar(db)); + + for (i, typevar) in typevars.clone().enumerate() { + // Only check legacy TypeVars; PEP 695 type parameters are already validated + // by `check_default_for_outer_scope_typevars` in the type parameter scope. + if !matches!( + typevar.kind(db), + TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec + ) { + continue; + } + + let Some(default_ty) = typevar.default_type(db) else { + continue; + }; + + let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let tvar = match t { + Type::TypeVar(tvar) => tvar.typevar(db), + Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, + _ => return None, + }; + if !typevars.clone().take(i).contains(&tvar) { + Some(tvar) + } else { + None + } + }); + + let Some(bad_typevar) = first_bad_tvar else { + continue; + }; + + let is_later_in_list = typevars.clone().skip(i).contains(&bad_typevar); + let node = last_definition.node(db, self.file(), self.module()); + + let primary_range = self.find_typevar_annotation_range(node, typevar); + + let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, primary_range) + else { + continue; + }; + let typevar_name = typevar.name(db); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid use of type variable `{typevar_name}`", + )); + + if is_later_in_list { + diagnostic.set_primary_message(format_args!( + "Default of `{typevar_name}` references later type parameter `{}`", + bad_typevar.name(db), + )); + diagnostic.set_concise_message(format_args!( + "Invalid use of type variable `{typevar_name}`: default of `{typevar_name}` \ + refers to later parameter `{}`", + bad_typevar.name(db) + )); + } else { + diagnostic.set_primary_message(format_args!( + "Default of `{typevar_name}` references out-of-scope type variable `{}`", + bad_typevar.name(db), + )); + diagnostic.set_concise_message(format_args!( + "Invalid use of type variable `{typevar_name}`: default of `{typevar_name}` \ + refers to out-of-scope type variable `{}`", + bad_typevar.name(db) + )); + } + + if let Some(typevar_definition) = typevar.definition(db) { + let file = typevar_definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + typevar_definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{typevar_name}` defined here")), + ); + } + + diagnostic + .info("See https://typing.python.org/en/latest/spec/generics.html#scoping-rules"); + } + } + + /// Check that legacy `TypeVar`s without defaults don't follow `TypeVar`s with defaults + /// in a function's generic context. + /// + /// This mirrors the class-level check using `report_invalid_type_param_order`, but for + /// function/method generic contexts using the `invalid-type-variable-default` lint. + fn check_legacy_typevar_ordering( + &self, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, + ) { + struct State<'db> { + typevar_with_default: TypeVarInstance<'db>, + invalid_later_tvars: Vec>, + } + + let db = self.db(); + + let Some(generic_context) = signature.generic_context else { + return; + }; + + let mut state: Option> = None; + + for bound_typevar in generic_context.variables(db) { + let typevar = bound_typevar.typevar(db); + + // Only check legacy TypeVars; PEP 695 ordering is validated by the parser. + if !matches!( + typevar.kind(db), + TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec + ) { + continue; + } + + let has_default = typevar.default_type(db).is_some(); + + if let Some(state) = state.as_mut() { + if !has_default { + state.invalid_later_tvars.push(typevar); } + } else if has_default { + state = Some(State { + typevar_with_default: typevar, + invalid_later_tvars: vec![], + }); } } + + let Some(state) = state else { + return; + }; + + if state.invalid_later_tvars.is_empty() { + return; + } + + let node = last_definition.node(db, self.file(), self.module()); + + let primary_range = self.find_typevar_annotation_range(node, state.invalid_later_tvars[0]); + + let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, primary_range) + else { + return; + }; + + let mut diagnostic = builder.into_diagnostic( + "Type parameters without defaults cannot follow type parameters with defaults", + ); + + let typevar_with_default_name = state.typevar_with_default.name(db); + + diagnostic.set_concise_message(format_args!( + "Type parameter `{}` without a default cannot follow \ + earlier parameter `{typevar_with_default_name}` with a default", + state.invalid_later_tvars[0].name(db), + )); + + if let [single_typevar] = &*state.invalid_later_tvars { + diagnostic.set_primary_message(format_args!( + "Type variable `{}` does not have a default", + single_typevar.name(db), + )); + } else { + let later_typevars = + format_enumeration(state.invalid_later_tvars.iter().map(|tv| tv.name(db))); + diagnostic.set_primary_message(format_args!( + "Type variables {later_typevars} do not have defaults", + )); + } + + let secondary_range = self.find_typevar_annotation_range(node, state.typevar_with_default); + + diagnostic.annotate( + self.context + .secondary(secondary_range) + .message(format_args!( + "Earlier TypeVar `{typevar_with_default_name}` has a default" + )), + ); + + for tvar in [state.typevar_with_default, state.invalid_later_tvars[0]] { + let Some(definition) = tvar.definition(db) else { + continue; + }; + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{}` defined here", tvar.name(db))), + ); + } } /// Iterate over all static class definitions (created using `class` statements) to check that @@ -4657,17 +4915,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(default_expr) = default.as_deref() { let default_ty = self.infer_type_expression(default_expr); - let bound_node = bound_node.map(|n| match n { - ast::Expr::Tuple(tuple) => BoundOrConstraintsNodes::Constraints(&tuple.elts), - _ => BoundOrConstraintsNodes::Bound(n), - }); - self.validate_typevar_default( - Some(&name.id), - bound_or_constraints, - default_ty, - default_expr, - bound_node, - ); + if !self.check_default_for_outer_scope_typevars(default_ty, default_expr, &name.id) { + let bound_node = bound_node.map(|n| match n { + ast::Expr::Tuple(tuple) => BoundOrConstraintsNodes::Constraints(&tuple.elts), + _ => BoundOrConstraintsNodes::Bound(n), + }); + self.validate_typevar_default( + Some(&name.id), + bound_or_constraints, + default_ty, + default_expr, + bound_node, + ); + } } self.deferred_state = previous_deferred_state; } @@ -4965,6 +5225,82 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Check if a PEP 695 type parameter's default references type variables from an outer scope. + /// + /// Returns `true` if such a reference was found and a diagnostic was emitted, + /// indicating that further default validation should be skipped. + /// + /// Note: this only handles PEP 695 type parameters in function and type alias scopes. + /// Class type parameter scopes are skipped here because out-of-scope references + /// are validated at the class level via `report_invalid_typevar_default_reference`. + /// Legacy `TypeVar`s are validated by `check_legacy_typevar_defaults`. + fn check_default_for_outer_scope_typevars( + &self, + default_ty: Type<'db>, + default_node: &ast::Expr, + typevar_name: &str, + ) -> bool { + let db = self.db(); + + // Determine the expected binding context from the current type parameter scope. + // Only check function and type alias scopes; class scopes are handled separately + // when processing the class definition. + let expected_binding_def = match self.scope().node(db) { + NodeWithScopeKind::FunctionTypeParameters(function) => { + self.index.expect_single_definition(function) + } + NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => { + self.index.expect_single_definition(type_alias) + } + _ => return false, + }; + let expected_binding = BindingContext::Definition(expected_binding_def); + + let outer_tv = find_over_type(db, default_ty, false, |ty| { + if let Type::TypeVar(bound_tv) = ty + && bound_tv.binding_context(db) != expected_binding + { + Some(bound_tv) + } else { + None + } + }); + + let Some(outer_tv) = outer_tv else { + return false; + }; + let outer_typevar = outer_tv.typevar(db); + let outer_name = outer_typevar.name(db); + let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_node) + else { + return false; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid default for type parameter `{typevar_name}`" + )); + diagnostic.set_primary_message(format_args!( + "`{outer_name}` is a type parameter bound in an outer scope" + )); + diagnostic.set_concise_message(format_args!( + "Type parameter `{typevar_name}` cannot use \ + outer-scope type parameter `{outer_name}` as its default" + )); + if let Some(definition) = outer_typevar.definition(db) { + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{outer_name}` defined here")), + ); + } + diagnostic.info("See https://typing.python.org/en/latest/spec/generics.html#scoping-rules"); + + true + } + fn infer_paramspec_definition( &mut self, node: &ast::TypeParamParamSpec, @@ -5003,7 +5339,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let ast::TypeParamParamSpec { range: _, node_index: _, - name: _, + name, default: Some(default), } = node else { @@ -5011,22 +5347,26 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let previous_deferred_state = std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred); - self.infer_paramspec_default(default); + self.infer_paramspec_default(default, Some(&name.id)); self.deferred_state = previous_deferred_state; } - fn infer_paramspec_default(&mut self, default_expr: &ast::Expr) { + fn infer_paramspec_default(&mut self, default_expr: &ast::Expr, paramspec_name: Option<&str>) { let previously_allowed_paramspec = self .inference_flags .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true); - self.infer_paramspec_default_impl(default_expr); + self.infer_paramspec_default_impl(default_expr, paramspec_name); self.inference_flags.set( InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, previously_allowed_paramspec, ); } - fn infer_paramspec_default_impl(&mut self, default_expr: &ast::Expr) { + fn infer_paramspec_default_impl( + &mut self, + default_expr: &ast::Expr, + paramspec_name: Option<&str>, + ) { match default_expr { ast::Expr::EllipsisLiteral(ellipsis) => { let ty = self.infer_ellipsis_literal_expression(ellipsis); @@ -5048,6 +5388,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } ast::Expr::Name(_) => { let ty = self.infer_type_expression(default_expr); + if let Some(name) = paramspec_name + && self.check_default_for_outer_scope_typevars(ty, default_expr, name) + { + return; + } let is_paramspec = match ty { Type::TypeVar(typevar) => typevar.is_paramspec(self.db()), Type::KnownInstance(known_instance) => { @@ -7617,7 +7962,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { known_class, Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) ) { - self.infer_paramspec_default(&default.value); + // Pass `None` for the name: the outer-scope typevar check inside + // `infer_paramspec_default` is only relevant for PEP 695 type parameter + // scopes. Legacy ParamSpec definitions live at module/class-body scope, + // so the check would be a no-op here. Out-of-scope defaults for legacy + // typevars are instead validated by `check_legacy_typevar_defaults` + // (for functions) and `report_invalid_typevar_default_reference` + // (for classes). + self.infer_paramspec_default(&default.value, None); } else { let default_ty = self.infer_type_expression(&default.value); let bound_or_constraints_node = arguments diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 7dfe530bed89d..74b20481105df 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -35,6 +35,20 @@ impl<'db> Type<'db> { any_over_type(db, self, false, |ty| matches!(ty, Type::TypeVar(_))) } + pub(crate) fn references_typevar( + self, + db: &'db dyn Db, + typevar_id: TypeVarIdentity<'db>, + ) -> bool { + any_over_type(db, self, false, |ty| match ty { + Type::TypeVar(bound_typevar) => typevar_id == bound_typevar.typevar(db).identity(db), + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { + typevar_id == typevar.identity(db) + } + _ => false, + }) + } + pub(crate) fn has_non_self_typevar(self, db: &'db dyn Db) -> bool { any_over_type( db, @@ -593,7 +607,8 @@ impl<'db> TypeVarInstance<'db> { ) -> Option> { let default = self.lazy_default_unchecked(db)?; - // Unlike bounds/constraints, default types are allowed to be generic (https://peps.python.org/pep-0696/#using-another-type-parameter-as-default). + // Unlike bounds/constraints, default types are allowed to be generic + // (https://typing.python.org/en/latest/spec/generics.html#defaults-for-type-parameters). // Here we simply check for non-self-referential. // TODO: We should also check for non-forward references. if self.type_is_self_referential(db, default, visitor) {