From 670fffef37218a53a9079f3adea7a6f1b1a49b76 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Fri, 5 Sep 2025 05:20:59 +0900 Subject: [PATCH 01/11] [`ruff`] Use helper function for empty f-string detection in `in-empty-collection` (`RUF060`) (#20249) ## Summary Fixes #20238 Replace inline f-string emptiness check with `is_empty_f_string` helper function --- .../ruff_linter/resources/test/fixtures/ruff/RUF060.py | 4 ++++ .../src/rules/ruff/rules/in_empty_collection.rs | 7 ++----- ...uff_linter__rules__ruff__tests__RUF060_RUF060.py.snap | 9 +++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py index c3b96c6b7b..78b5888207 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py @@ -42,3 +42,7 @@ b"a" in bytes("a", "utf-8") 1 in set(set([1])) '' in {""} frozenset() in {frozenset()} + +# https://github.com/astral-sh/ruff/issues/20238 +"b" in f"" "" # Error +"b" in f"" "x" # OK diff --git a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs index 30f4ff8bc7..ec3257d9ec 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{self as ast, CmpOp, Expr}; +use ruff_python_ast::{self as ast, CmpOp, Expr, helpers::is_empty_f_string}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; @@ -75,10 +75,7 @@ fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool { Expr::Dict(ast::ExprDict { items, .. }) => items.is_empty(), Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(), Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(), - Expr::FString(s) => s - .value - .elements() - .all(|elt| elt.as_literal().is_some_and(|elt| elt.is_empty())), + Expr::FString(s) => is_empty_f_string(s), Expr::Call(ast::ExprCall { func, arguments, diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap index 20e8cf9f42..cd8375a506 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap @@ -251,3 +251,12 @@ RUF060 Unnecessary membership test on empty collection 25 | 26 | # OK | + +RUF060 Unnecessary membership test on empty collection + --> RUF060.py:47:1 + | +46 | # https://github.com/astral-sh/ruff/issues/20238 +47 | "b" in f"" "" # Error + | ^^^^^^^^^^^^^ +48 | "b" in f"" "x" # OK + | From 08c1d3660c37db71d7a57cd3d7de9956ebee1ef7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 4 Sep 2025 15:28:33 -0700 Subject: [PATCH 02/11] [ty] Narrow specialized generics using isinstance() (#20256) Closes astral-sh/ty#456. Part of astral-sh/ty#994. After all the foundational work, this is only a small change, but let's see if it exposes any unresolved issues. --- .../mdtest/exhaustiveness_checking.md | 8 +- .../resources/mdtest/narrow/isinstance.md | 85 +++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 20 ++++- crates/ty_python_semantic/src/types/narrow.rs | 4 +- 4 files changed, 106 insertions(+), 11 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 974d0be94f..53d272b834 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -281,12 +281,10 @@ def if_else_exhaustive(x: A[D] | B[E] | C[F]): elif isinstance(x, C): pass else: - # TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456) - no_diagnostic_here # error: [unresolved-reference] - assert_never(x) # error: [type-assertion-failure] + no_diagnostic_here + assert_never(x) -# TODO: false-positive diagnostic (https://github.com/astral-sh/ty/issues/456) -def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type] +def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: if isinstance(x, A): return 0 elif isinstance(x, B): diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 17fde60063..d28d261fb1 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -308,3 +308,88 @@ def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])]( return (y, z) ``` + +## Narrowing with generics + +```toml +[environment] +python-version = "3.12" +``` + +Narrowing to a generic class using `isinstance()` uses the top materialization of the generic. With +a covariant generic, this is equivalent to using the upper bound of the type parameter (by default, +`object`): + +```py +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, Covariant): + reveal_type(x) # revealed: Covariant[object] + reveal_type(x.get()) # revealed: object +``` + +Similarly, contravariant type parameters use their lower bound of `Never`: + +```py +class Contravariant[T]: + def push(self, x: T) -> None: ... + +def _(x: object): + if isinstance(x, Contravariant): + reveal_type(x) # revealed: Contravariant[Never] + # error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`" + x.push(42) +``` + +Invariant generics are trickiest. The top materialization, conceptually the type that includes all +instances of the generic class regardless of the type parameter, cannot be represented directly in +the type system, so we represent it with the internal `Top[]` special form. + +```py +class Invariant[T]: + def push(self, x: T) -> None: ... + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, Invariant): + reveal_type(x) # revealed: Top[Invariant[Unknown]] + reveal_type(x.get()) # revealed: object + # error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`" + x.push(42) +``` + +When more complex types are involved, the `Top[]` type may get simplified away. + +```py +def _(x: list[int] | set[str]): + if isinstance(x, list): + reveal_type(x) # revealed: list[int] + else: + reveal_type(x) # revealed: set[str] +``` + +Though if the types involved are not disjoint bases, we necessarily keep a more complex type. + +```py +def _(x: Invariant[int] | Covariant[str]): + if isinstance(x, Invariant): + reveal_type(x) # revealed: Invariant[int] | (Covariant[str] & Top[Invariant[Unknown]]) + else: + reveal_type(x) # revealed: Covariant[str] & ~Top[Invariant[Unknown]] +``` + +The behavior of `issubclass()` is similar. + +```py +def _(x: type[object], y: type[object], z: type[object]): + if issubclass(x, Covariant): + reveal_type(x) # revealed: type[Covariant[object]] + if issubclass(y, Contravariant): + reveal_type(y) # revealed: type[Contravariant[Never]] + if issubclass(z, Invariant): + reveal_type(z) # revealed: type[Top[Invariant[Unknown]]] +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 60ab729d32..d4a5894053 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -29,10 +29,10 @@ use crate::types::typed_dict::typed_dict_params_from_class_def; use crate::types::{ ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, - IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, NormalizedVisitor, - PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, - TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, UnionBuilder, - VarianceInferable, declaration_type, infer_definition_types, + IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, + NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, + TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, + UnionBuilder, VarianceInferable, declaration_type, infer_definition_types, }; use crate::{ Db, FxIndexMap, FxOrderSet, Program, @@ -1470,6 +1470,18 @@ impl<'db> ClassLiteral<'db> { }) } + pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context + .default_specialization(db, self.known(db)) + .materialize_impl( + db, + MaterializationKind::Top, + &ApplyTypeMappingVisitor::default(), + ) + }) + } + /// Returns the default specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// applies the default specialization to the class's typevars. diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index fcfae4851a..0c6beb2b7d 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -174,10 +174,10 @@ impl ClassInfoConstraintFunction { fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { let constraint_fn = |class: ClassLiteral<'db>| match self { ClassInfoConstraintFunction::IsInstance => { - Type::instance(db, class.default_specialization(db)) + Type::instance(db, class.top_materialization(db)) } ClassInfoConstraintFunction::IsSubclass => { - SubclassOfType::from(db, class.default_specialization(db)) + SubclassOfType::from(db, class.top_materialization(db)) } }; From 888a22e84972981ed1b9f79c00c27b259589e9ba Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 4 Sep 2025 23:34:37 +0100 Subject: [PATCH 03/11] [ty] Reduce false positives for `ParamSpec`s and `TypeVarTuple`s (#20239) --- .../annotations/unsupported_special_forms.md | 17 ++++++ crates/ty_python_semantic/src/types/infer.rs | 56 +++++++++---------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 3b10945df9..8a6f499655 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -28,6 +28,23 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P. class Foo: def method(self, x: Self): reveal_type(x) # revealed: Self@method + +def ex2(msg: str): + def wrapper(fn: Callable[P, R_co]) -> Callable[P, R_co]: + def wrapped(*args: P.args, **kwargs: P.kwargs) -> R_co: + print(msg) + return fn(*args, **kwargs) + return wrapped + return wrapper + +def ex3(msg: str): + P = ParamSpec("P") + def wrapper(fn: Callable[P, R_co]) -> Callable[P, R_co]: + def wrapped(*args: P.args, **kwargs: P.kwargs) -> R_co: + print(msg) + return fn(*args, **kwargs) + return wrapped + return wrapper ``` ## Type expressions diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 83c974ff4f..8a19e88624 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -125,6 +125,7 @@ use crate::types::typed_dict::{ validate_typed_dict_key_assignment, }; use crate::types::unpacker::{UnpackResult, Unpacker}; +use crate::types::visitor::any_over_type; use crate::types::{ CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, @@ -9271,26 +9272,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let typevars: Result, GenericContextError> = typevars .iter() - .map(|typevar| match typevar { - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => bind_typevar( - self.db(), - self.module(), - self.index, - self.scope().file_scope_id(self.db()), - self.typevar_binding_context, - *typevar, - ) - .ok_or(GenericContextError::InvalidArgument), - Type::Dynamic(DynamicType::TodoUnpack) => Err(GenericContextError::NotYetSupported), - Type::NominalInstance(nominal) - if matches!( + .map(|typevar| { + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = typevar { + bind_typevar( + self.db(), + self.module(), + self.index, + self.scope().file_scope_id(self.db()), + self.typevar_binding_context, + *typevar, + ) + .ok_or(GenericContextError::InvalidArgument) + } else if any_over_type(self.db(), *typevar, &|ty| match ty { + Type::Dynamic(DynamicType::TodoUnpack) => true, + Type::NominalInstance(nominal) => matches!( nominal.class(self.db()).known(self.db()), Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec) - ) => - { + ), + _ => false, + }) { Err(GenericContextError::NotYetSupported) - } - _ => { + } else { if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node) { @@ -11265,18 +11267,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // `Callable[]`. return None; } - match self.infer_name_load(name) { - Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => { - return Some(Parameters::todo()); - } - Type::NominalInstance(nominal) - if nominal - .class(self.db()) - .is_known(self.db(), KnownClass::ParamSpec) => - { - return Some(Parameters::todo()); - } - _ => {} + if any_over_type(self.db(), self.infer_name_load(name), &|ty| match ty { + Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => true, + Type::NominalInstance(nominal) => nominal + .class(self.db()) + .is_known(self.db(), KnownClass::ParamSpec), + _ => false, + }) { + return Some(Parameters::todo()); } } _ => {} From a24a4b55eef1794b1ee575352aea9746e30da5dd Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 5 Sep 2025 00:37:42 +0200 Subject: [PATCH 04/11] [ty] TypedDict: Add support for `typing.ReadOnly` (#20241) ## Summary Add support for `typing.ReadOnly` as a type qualifier to mark `TypedDict` fields as being read-only. If you try to mutate them, you get a new diagnostic: image ## Test Plan * New Markdown tests * The typing conformance changes are all correct. There are some false negatives, but those are related to the missing support for the functional form of `TypedDict`, or to overriding of fields via inheritance. Both of these topics are tracked in https://github.com/astral-sh/ty/issues/154 --- ...ict`_-_Diagnostics_(e5289abf5c570c29).snap | 23 +++++++++ .../resources/mdtest/typed_dict.md | 34 +++++++++++-- crates/ty_python_semantic/src/place.rs | 5 ++ crates/ty_python_semantic/src/types/class.rs | 49 +++++++++++++++++-- .../src/types/typed_dict.rs | 27 ++++++++++ 5 files changed, 131 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap index 22a0861fca..42dfc0e33d 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap @@ -37,6 +37,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md 23 | 24 | def write_to_non_literal_string_key(person: Person, str_key: str): 25 | person[str_key] = "Alice" # error: [invalid-key] +26 | from typing_extensions import ReadOnly +27 | +28 | class Employee(TypedDict): +29 | id: ReadOnly[int] +30 | name: str +31 | +32 | def write_to_readonly_key(employee: Employee): +33 | employee["id"] = 42 # error: [invalid-assignment] ``` # Diagnostics @@ -127,7 +135,22 @@ error[invalid-key]: Cannot access `Person` with a key of type `str`. Only string 24 | def write_to_non_literal_string_key(person: Person, str_key: str): 25 | person[str_key] = "Alice" # error: [invalid-key] | ^^^^^^^ +26 | from typing_extensions import ReadOnly | info: rule `invalid-key` is enabled by default ``` + +``` +error[invalid-assignment]: Can not assign to key "id" on TypedDict `Employee` + --> src/mdtest_snippet.py:33:5 + | +32 | def write_to_readonly_key(employee: Employee): +33 | employee["id"] = 42 # error: [invalid-assignment] + | -------- ^^^^ key is marked read-only + | | + | TypedDict `Employee` + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 9c498e501c..2bd173a8ed 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -444,8 +444,7 @@ def _(person: Person, unknown_key: Any): ## `ReadOnly` -`ReadOnly` is not supported yet, but this test makes sure that we do not emit any false positive -diagnostics: +Assignments to keys that are marked `ReadOnly` will produce an error: ```py from typing_extensions import TypedDict, ReadOnly, Required @@ -458,10 +457,26 @@ class Person(TypedDict, total=False): alice: Person = {"id": 1, "name": "Alice", "age": 30} alice["age"] = 31 # okay -# TODO: this should be an error +# error: [invalid-assignment] "Can not assign to key "id" on TypedDict `Person`: key is marked read-only" alice["id"] = 2 ``` +This also works if all fields on a `TypedDict` are `ReadOnly`, in which case we synthesize a +`__setitem__` method with a `key` type of `Never`: + +```py +class Config(TypedDict): + host: ReadOnly[str] + port: ReadOnly[int] + +config: Config = {"host": "localhost", "port": 8080} + +# error: [invalid-assignment] "Can not assign to key "host" on TypedDict `Config`: key is marked read-only" +config["host"] = "127.0.0.1" +# error: [invalid-assignment] "Can not assign to key "port" on TypedDict `Config`: key is marked read-only" +config["port"] = 80 +``` + ## Methods on `TypedDict` ```py @@ -846,6 +861,19 @@ def write_to_non_literal_string_key(person: Person, str_key: str): person[str_key] = "Alice" # error: [invalid-key] ``` +Assignment to `ReadOnly` keys: + +```py +from typing_extensions import ReadOnly + +class Employee(TypedDict): + id: ReadOnly[int] + name: str + +def write_to_readonly_key(employee: Employee): + employee["id"] = 42 # error: [invalid-assignment] +``` + ## Import aliases `TypedDict` can be imported with aliases and should work correctly: diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 06c81c093f..ba9d7d47a3 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -589,6 +589,11 @@ impl<'db> PlaceAndQualifiers<'db> { self.qualifiers.contains(TypeQualifiers::NOT_REQUIRED) } + /// Returns `true` if the place has a `ReadOnly` type qualifier. + pub(crate) fn is_read_only(&self) -> bool { + self.qualifiers.contains(TypeQualifiers::READ_ONLY) + } + /// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type. pub(crate) fn is_bare_final(&self) -> Option { match self { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d4a5894053..315c959853 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1271,6 +1271,8 @@ pub(crate) enum FieldKind<'db> { TypedDict { /// Whether this field is required is_required: bool, + /// Whether this field is marked read-only + is_read_only: bool, }, } @@ -1292,7 +1294,14 @@ impl Field<'_> { FieldKind::Dataclass { init, default_ty, .. } => default_ty.is_none() && *init, - FieldKind::TypedDict { is_required } => *is_required, + FieldKind::TypedDict { is_required, .. } => *is_required, + } + } + + pub(crate) const fn is_read_only(&self) -> bool { + match &self.kind { + FieldKind::TypedDict { is_read_only, .. } => *is_read_only, + _ => false, } } } @@ -2276,8 +2285,36 @@ impl<'db> ClassLiteral<'db> { (CodeGeneratorKind::TypedDict, "__setitem__") => { let fields = self.fields(db, specialization, field_policy); - // Add (key type, value type) overloads for all TypedDict items ("fields"): - let overloads = fields.iter().map(|(name, field)| { + // Add (key type, value type) overloads for all TypedDict items ("fields") that are not read-only: + + let mut writeable_fields = fields + .iter() + .filter(|(_, field)| !field.is_read_only()) + .peekable(); + + if writeable_fields.peek().is_none() { + // If there are no writeable fields, synthesize a `__setitem__` that takes + // a `key` of type `Never` to signal that no keys are accepted. This leads + // to slightly more user-friendly error messages compared to returning an + // empty overload set. + return Some(Type::Callable(CallableType::new( + db, + CallableSignature::single(Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(Type::Never), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::any()), + ]), + Some(Type::none(db)), + )), + true, + ))); + } + + let overloads = writeable_fields.map(|(name, field)| { let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str())); Signature::new( @@ -2680,7 +2717,11 @@ impl<'db> ClassLiteral<'db> { .expect("TypedDictParams should be available for CodeGeneratorKind::TypedDict") .contains(TypedDictParams::TOTAL) }; - FieldKind::TypedDict { is_required } + + FieldKind::TypedDict { + is_required, + is_read_only: attr.is_read_only(), + } } }; diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 8fe8f500bc..e95535271e 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -122,6 +122,10 @@ impl TypedDictAssignmentKind { Self::Constructor => &INVALID_ARGUMENT_TYPE, } } + + const fn is_subscript(self) -> bool { + matches!(self, Self::Subscript) + } } /// Validates assignment of a value to a specific key on a `TypedDict`. @@ -153,6 +157,29 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>( return false; }; + if assignment_kind.is_subscript() && item.is_read_only() { + if let Some(builder) = + context.report_lint(assignment_kind.diagnostic_type(), key_node.into()) + { + let typed_dict_ty = Type::TypedDict(typed_dict); + let typed_dict_d = typed_dict_ty.display(db); + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Can not assign to key \"{key}\" on TypedDict `{typed_dict_d}`", + )); + + diagnostic.set_primary_message(format_args!("key is marked read-only")); + + diagnostic.annotate( + context + .secondary(typed_dict_node.into()) + .message(format_args!("TypedDict `{typed_dict_d}`")), + ); + } + + return false; + } + // Key exists, check if value type is assignable to declared type if value_ty.is_assignable_to(db, item.declared_ty) { return true; From 7509d376eb9ce23a386fc93ad7dcef7565f2f4a8 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 5 Sep 2025 09:19:14 +0200 Subject: [PATCH 05/11] [ty] Minor: 'can not' => cannot (#20260) --- ...t.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap | 2 +- crates/ty_python_semantic/resources/mdtest/typed_dict.md | 6 +++--- crates/ty_python_semantic/src/types/typed_dict.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap index 42dfc0e33d..222a3c52f5 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap @@ -142,7 +142,7 @@ info: rule `invalid-key` is enabled by default ``` ``` -error[invalid-assignment]: Can not assign to key "id" on TypedDict `Employee` +error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee` --> src/mdtest_snippet.py:33:5 | 32 | def write_to_readonly_key(employee: Employee): diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 2bd173a8ed..f7e3edab4f 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -457,7 +457,7 @@ class Person(TypedDict, total=False): alice: Person = {"id": 1, "name": "Alice", "age": 30} alice["age"] = 31 # okay -# error: [invalid-assignment] "Can not assign to key "id" on TypedDict `Person`: key is marked read-only" +# error: [invalid-assignment] "Cannot assign to key "id" on TypedDict `Person`: key is marked read-only" alice["id"] = 2 ``` @@ -471,9 +471,9 @@ class Config(TypedDict): config: Config = {"host": "localhost", "port": 8080} -# error: [invalid-assignment] "Can not assign to key "host" on TypedDict `Config`: key is marked read-only" +# error: [invalid-assignment] "Cannot assign to key "host" on TypedDict `Config`: key is marked read-only" config["host"] = "127.0.0.1" -# error: [invalid-assignment] "Can not assign to key "port" on TypedDict `Config`: key is marked read-only" +# error: [invalid-assignment] "Cannot assign to key "port" on TypedDict `Config`: key is marked read-only" config["port"] = 80 ``` diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index e95535271e..b3fe37f867 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -165,7 +165,7 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>( let typed_dict_d = typed_dict_ty.display(db); let mut diagnostic = builder.into_diagnostic(format_args!( - "Can not assign to key \"{key}\" on TypedDict `{typed_dict_d}`", + "Cannot assign to key \"{key}\" on TypedDict `{typed_dict_d}`", )); diagnostic.set_primary_message(format_args!("key is marked read-only")); From 9e45bfa9fd730dc6a95bd94ced51147cc507cfe6 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 5 Sep 2025 10:12:40 +0200 Subject: [PATCH 06/11] [ty] Cover full range of annotated assignments (#20261) ## Summary An annotated assignment `name: annotation` without a right-hand side was previously not covered by the range returned from `DefinitionKind::full_range`, because we did expand the range to include the right-hand side (if there was one), but failed to include the annotation. ## Test Plan Updated snapshot tests --- ....NamedTuple`_-_Definition_(bbf79630502e65e9).snap | 12 ++++++------ .../src/semantic_index/definition.rs | 11 ++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap index 666d2bc2c5..d7effd9556 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap @@ -47,7 +47,7 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow | --------------------- Earlier field `altitude` defined here with a default value 5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitud… 6 | latitude: float - | ^^^^^^^^ Field `latitude` defined here without a default value + | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value 7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitu… 8 | longitude: float | @@ -66,7 +66,7 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow 6 | latitude: float 7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longit… 8 | longitude: float - | ^^^^^^^^^ Field `longitude` defined here without a default value + | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value 9 | 10 | class StrangeLocation(NamedTuple): | @@ -83,7 +83,7 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow 14 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value 15 | latitude: float # error: [invalid-named-tuple] - | ^^^^^^^^ Field `latitude` defined here without a default value + | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value 16 | longitude: float # error: [invalid-named-tuple] | info: rule `invalid-named-tuple` is enabled by default @@ -100,7 +100,7 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow | --------------------- Earlier field `altitude` defined here with a default value 15 | latitude: float # error: [invalid-named-tuple] 16 | longitude: float # error: [invalid-named-tuple] - | ^^^^^^^^^ Field `longitude` defined here without a default value + | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value 17 | 18 | class VeryStrangeLocation(NamedTuple): | @@ -115,7 +115,7 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow 18 | class VeryStrangeLocation(NamedTuple): 19 | altitude: float = 0.0 20 | latitude: float # error: [invalid-named-tuple] - | ^^^^^^^^ Field `latitude` defined here without a default value + | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value 21 | longitude: float # error: [invalid-named-tuple] 22 | altitude: float = 0.0 | @@ -131,7 +131,7 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow 19 | altitude: float = 0.0 20 | latitude: float # error: [invalid-named-tuple] 21 | longitude: float # error: [invalid-named-tuple] - | ^^^^^^^^^ Field `longitude` defined here without a default value + | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value 22 | altitude: float = 0.0 | info: Earlier field `altitude` was defined with a default value diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 2e2ff104de..0075c0ff41 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -769,13 +769,14 @@ impl DefinitionKind<'_> { target_range.cover(value_range) } DefinitionKind::AnnotatedAssignment(assign) => { - let target_range = assign.target.node(module).range(); + let mut full_range = assign.target.node(module).range(); + full_range = full_range.cover(assign.annotation.node(module).range()); + if let Some(ref value) = assign.value { - let value_range = value.node(module).range(); - target_range.cover(value_range) - } else { - target_range + full_range = full_range.cover(value.node(module).range()); } + + full_range } DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.node(module).range(), DefinitionKind::For(for_stmt) => for_stmt.target.node(module).range(), From 8ade6c4eaf0696709dfb27f676ecebe65b16faa1 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 5 Sep 2025 12:38:37 +0200 Subject: [PATCH 07/11] [ty] Add backreferences to TypedDict items in diagnostics (#20262) ## Summary Add backreferences to the original item declaration in TypedDict diagnostics. Thanks to @AlexWaygood for the suggestion. ## Test Plan Updated snapshots --- crates/ty/docs/rules.md | 128 +++++++++--------- ...ict`_-_Diagnostics_(e5289abf5c570c29).snap | 18 +++ crates/ty_python_semantic/src/place.rs | 40 +++--- crates/ty_python_semantic/src/types/class.rs | 60 ++------ .../src/types/diagnostic.rs | 20 +-- crates/ty_python_semantic/src/types/infer.rs | 6 +- .../src/types/typed_dict.rs | 23 ++++ 7 files changed, 146 insertions(+), 149 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index f0d36bf5ad..7904c1cb7f 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -36,7 +36,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L114) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L113) **What it does** @@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L158) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157) **What it does** @@ -88,7 +88,7 @@ f(int) # error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L184) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183) **What it does** @@ -117,7 +117,7 @@ a = 1 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L209) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208) **What it does** @@ -147,7 +147,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L235) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234) **What it does** @@ -177,7 +177,7 @@ class B(A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L300) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L299) **What it does** @@ -202,7 +202,7 @@ class B(A, A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L321) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L320) **What it does** @@ -306,7 +306,7 @@ def test(): -> "Literal[5]": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L524) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523) **What it does** @@ -334,7 +334,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L548) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L547) **What it does** @@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L353) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L352) **What it does** @@ -445,7 +445,7 @@ an atypical memory layout. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L593) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L592) **What it does** @@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L633) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632) **What it does** @@ -496,7 +496,7 @@ a: int = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1667) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1666) **What it does** @@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L655) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654) **What it does** @@ -562,7 +562,7 @@ asyncio.run(main()) Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L684) **What it does** @@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L736) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L735) **What it does** @@ -609,7 +609,7 @@ with 1: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L757) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L756) **What it does** @@ -636,7 +636,7 @@ a: str Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L780) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L779) **What it does** @@ -678,7 +678,7 @@ except ZeroDivisionError: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L816) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L815) **What it does** @@ -709,7 +709,7 @@ class C[U](Generic[T]): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L568) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567) **What it does** @@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L842) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L841) **What it does** @@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L891) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L890) **What it does** @@ -803,7 +803,7 @@ class B(metaclass=f): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L498) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L497) **What it does** @@ -833,7 +833,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L918) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917) **What it does** @@ -881,7 +881,7 @@ def foo(x: int) -> int: ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L961) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960) **What it does** @@ -905,7 +905,7 @@ def f(a: int = ''): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L435) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L434) **What it does** @@ -937,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L980) Checks for `raise` statements that raise non-exceptions or use invalid @@ -984,7 +984,7 @@ def g(): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L614) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L613) **What it does** @@ -1007,7 +1007,7 @@ def func() -> int: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1024) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1023) **What it does** @@ -1061,7 +1061,7 @@ TODO #14889 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L870) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L869) **What it does** @@ -1086,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062) **What it does** @@ -1114,7 +1114,7 @@ TYPE_CHECKING = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1087) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086) **What it does** @@ -1142,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1139) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1138) **What it does** @@ -1174,7 +1174,7 @@ f(10) # Error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1111) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1110) **What it does** @@ -1206,7 +1206,7 @@ class C: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1167) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1166) **What it does** @@ -1239,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1195) **What it does** @@ -1262,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1766) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1765) **What it does** @@ -1293,7 +1293,7 @@ alice["age"] # KeyError Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1215) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1214) **What it does** @@ -1320,7 +1320,7 @@ func("string") # error: [no-matching-overload] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1238) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1237) **What it does** @@ -1342,7 +1342,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1256) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1255) **What it does** @@ -1366,7 +1366,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1307) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1306) **What it does** @@ -1420,7 +1420,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1643) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642) **What it does** @@ -1448,7 +1448,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1398) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397) **What it does** @@ -1475,7 +1475,7 @@ class B(A): ... # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1443) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1442) **What it does** @@ -1500,7 +1500,7 @@ f("foo") # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1421) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1420) **What it does** @@ -1526,7 +1526,7 @@ def _(x: int): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1464) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463) **What it does** @@ -1570,7 +1570,7 @@ class A: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1521) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1520) **What it does** @@ -1595,7 +1595,7 @@ f(x=1, y=2) # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541) **What it does** @@ -1621,7 +1621,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563) **What it does** @@ -1644,7 +1644,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1583) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1582) **What it does** @@ -1667,7 +1667,7 @@ print(x) # NameError: name 'x' is not defined Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1276) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1275) **What it does** @@ -1702,7 +1702,7 @@ b1 < b2 < b1 # exception raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1602) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1601) **What it does** @@ -1728,7 +1728,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1624) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623) **What it does** @@ -1751,7 +1751,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L463) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L462) **What it does** @@ -1790,7 +1790,7 @@ class SubProto(BaseProto, Protocol): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L279) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L278) **What it does** @@ -1843,7 +1843,7 @@ a = 20 / 0 # type: ignore Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1328) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1327) **What it does** @@ -1869,7 +1869,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L132) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L131) **What it does** @@ -1899,7 +1899,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1350) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1349) **What it does** @@ -1929,7 +1929,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1695) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1694) **What it does** @@ -1954,7 +1954,7 @@ cast(int, f()) # Redundant Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1503) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1502) **What it does** @@ -2005,7 +2005,7 @@ a = 20 / 0 # ty: ignore[division-by-zero] Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1716) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1715) **What it does** @@ -2059,7 +2059,7 @@ def g(): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L703) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702) **What it does** @@ -2096,7 +2096,7 @@ class D(C): ... # error: [unsupported-base] Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L261) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260) **What it does** @@ -2118,7 +2118,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1376) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375) **What it does** diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap index 222a3c52f5..b80700fa08 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap @@ -108,6 +108,16 @@ error[invalid-assignment]: Invalid assignment to key "age" with declared type `i 20 | 21 | def write_to_non_existing_key(person: Person): | +info: Item declaration + --> src/mdtest_snippet.py:5:5 + | +3 | class Person(TypedDict): +4 | name: str +5 | age: int | None + | --------------- Item declared here +6 | +7 | def access_invalid_literal_string_key(person: Person): + | info: rule `invalid-assignment` is enabled by default ``` @@ -151,6 +161,14 @@ error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee` | | | TypedDict `Employee` | +info: Item declaration + --> src/mdtest_snippet.py:29:5 + | +28 | class Employee(TypedDict): +29 | id: ReadOnly[int] + | ----------------- Read-only item declared here +30 | name: str + | info: rule `invalid-assignment` is enabled by default ``` diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index ba9d7d47a3..d140882877 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -486,6 +486,9 @@ type DeclaredTypeAndConflictingTypes<'db> = ( pub(crate) struct PlaceFromDeclarationsResult<'db> { place_and_quals: PlaceAndQualifiers<'db>, conflicting_types: Option>>>, + /// Contains `Some(declaration)` if the declared type originates from exactly one declaration. + /// This field is used for backreferences in diagnostics. + pub(crate) single_declaration: Option>, } impl<'db> PlaceFromDeclarationsResult<'db> { @@ -496,6 +499,7 @@ impl<'db> PlaceFromDeclarationsResult<'db> { PlaceFromDeclarationsResult { place_and_quals, conflicting_types: Some(conflicting_types), + single_declaration: None, } } @@ -513,21 +517,6 @@ impl<'db> PlaceFromDeclarationsResult<'db> { } } -impl<'db> From> for PlaceFromDeclarationsResult<'db> { - fn from(place_and_quals: PlaceAndQualifiers<'db>) -> Self { - PlaceFromDeclarationsResult { - place_and_quals, - conflicting_types: None, - } - } -} - -impl<'db> From> for PlaceFromDeclarationsResult<'db> { - fn from(place: Place<'db>) -> Self { - PlaceFromDeclarationsResult::from(PlaceAndQualifiers::from(place)) - } -} - /// A type with declaredness information, and a set of type qualifiers. /// /// This is used to represent the result of looking up the declared type. Consider this @@ -1216,6 +1205,8 @@ fn place_from_declarations_impl<'db>( let reachability_constraints = declarations.reachability_constraints; let boundness_analysis = declarations.boundness_analysis; let mut declarations = declarations.peekable(); + let mut first_declaration = None; + let mut exactly_one_declaration = false; let is_non_exported = |declaration: Definition<'db>| { requires_explicit_reexport.is_yes() && !is_reexported(db, declaration) @@ -1246,6 +1237,13 @@ fn place_from_declarations_impl<'db>( return None; } + if first_declaration.is_none() { + first_declaration = Some(declaration); + exactly_one_declaration = true; + } else { + exactly_one_declaration = false; + } + let static_reachability = reachability_constraints.evaluate(db, predicates, reachability_constraint); @@ -1302,10 +1300,18 @@ fn place_from_declarations_impl<'db>( if let Some(conflicting) = conflicting { PlaceFromDeclarationsResult::conflict(place_and_quals, conflicting) } else { - place_and_quals.into() + PlaceFromDeclarationsResult { + place_and_quals, + conflicting_types: None, + single_declaration: first_declaration.filter(|_| exactly_one_declaration), + } } } else { - Place::Unbound.into() + PlaceFromDeclarationsResult { + place_and_quals: Place::Unbound.into(), + conflicting_types: None, + single_declaration: None, + } } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 315c959853..501bea8844 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -9,12 +9,10 @@ use super::{ use crate::FxOrderMap; use crate::module_resolver::KnownModule; use crate::semantic_index::definition::{Definition, DefinitionState}; -use crate::semantic_index::place::ScopedPlaceId; use crate::semantic_index::scope::NodeWithScopeKind; use crate::semantic_index::symbol::Symbol; use crate::semantic_index::{ - BindingWithConstraints, DeclarationWithConstraint, SemanticIndex, attribute_declarations, - attribute_scopes, + DeclarationWithConstraint, SemanticIndex, attribute_declarations, attribute_scopes, }; use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension}; use crate::types::context::InferContext; @@ -1283,6 +1281,9 @@ pub(crate) struct Field<'db> { pub(crate) declared_ty: Type<'db>, /// Kind-specific metadata for this field pub(crate) kind: FieldKind<'db>, + /// The original declaration of this field, if there is exactly one. + /// This field is used for backreferences in diagnostics. + pub(crate) single_declaration: Option>, } impl Field<'_> { @@ -2666,7 +2667,9 @@ impl<'db> ClassLiteral<'db> { let symbol = table.symbol(symbol_id); - let attr = place_from_declarations(db, declarations).ignore_conflicting_declarations(); + let result = place_from_declarations(db, declarations.clone()); + let single_declaration = result.single_declaration; + let attr = result.ignore_conflicting_declarations(); if attr.is_class_var() { continue; } @@ -2728,6 +2731,7 @@ impl<'db> ClassLiteral<'db> { let mut field = Field { declared_ty: attr_ty.apply_optional_specialization(db, specialization), kind, + single_declaration, }; // Check if this is a KW_ONLY sentinel and mark subsequent fields as keyword-only @@ -3330,54 +3334,6 @@ impl<'db> ClassLiteral<'db> { .unwrap_or_else(|| class_name.end()), ) } - - pub(super) fn declarations_of_name( - self, - db: &'db dyn Db, - name: &str, - index: &'db SemanticIndex<'db>, - ) -> Option>> { - let class_body_scope = self.body_scope(db).file_scope_id(db); - let symbol_id = index.place_table(class_body_scope).symbol_id(name)?; - let use_def = index.use_def_map(class_body_scope); - Some(use_def.end_of_scope_declarations(ScopedPlaceId::Symbol(symbol_id))) - } - - pub(super) fn first_declaration_of_name( - self, - db: &'db dyn Db, - name: &str, - index: &'db SemanticIndex<'db>, - ) -> Option> { - self.declarations_of_name(db, name, index) - .into_iter() - .flatten() - .next() - } - - pub(super) fn bindings_of_name( - self, - db: &'db dyn Db, - name: &str, - index: &'db SemanticIndex<'db>, - ) -> Option>> { - let class_body_scope = self.body_scope(db).file_scope_id(db); - let symbol_id = index.place_table(class_body_scope).symbol_id(name)?; - let use_def = index.use_def_map(class_body_scope); - Some(use_def.end_of_scope_bindings(ScopedPlaceId::Symbol(symbol_id))) - } - - pub(super) fn first_binding_of_name( - self, - db: &'db dyn Db, - name: &str, - index: &'db SemanticIndex<'db>, - ) -> Option> { - self.bindings_of_name(db, name, index) - .into_iter() - .flatten() - .next() - } } impl<'db> From> for Type<'db> { diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index a9b93de2e9..f7f5587d6a 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -6,7 +6,6 @@ use super::{ add_inferred_python_version_hint_to_diagnostic, }; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; -use crate::semantic_index::SemanticIndex; use crate::semantic_index::definition::Definition; use crate::semantic_index::place::{PlaceTable, ScopedPlaceId}; use crate::suppression::FileSuppressionId; @@ -2887,16 +2886,13 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'db>( context: &InferContext<'db, '_>, class: ClassLiteral<'db>, - index: &'db SemanticIndex<'db>, - field_name: &str, - field_with_default: &str, + (field, field_def): &(Name, Option>), + (field_with_default, field_with_default_def): &(Name, Option>), ) { let db = context.db(); let module = context.module(); - let diagnostic_range = class - .first_declaration_of_name(db, field_name, index) - .and_then(|definition| definition.declaration.definition()) + let diagnostic_range = field_def .map(|definition| definition.kind(db).full_range(module)) .unwrap_or_else(|| class.header_range(db)); @@ -2908,13 +2904,11 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<' )); diagnostic.set_primary_message(format_args!( - "Field `{field_name}` defined here without a default value" + "Field `{field}` defined here without a default value", )); - let Some(field_with_default_range) = class - .first_binding_of_name(db, field_with_default, index) - .and_then(|definition| definition.binding.definition()) - .map(|definition| definition.kind(db).full_range(module)) + let Some(field_with_default_range) = + field_with_default_def.map(|definition| definition.kind(db).full_range(module)) else { return; }; @@ -2933,7 +2927,7 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<' ); } else { diagnostic.info(format_args!( - "Earlier field `{field_with_default}` was defined with a default value" + "Earlier field `{field_with_default}` was defined with a default value", )); } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 8a19e88624..8dc5379e0e 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1191,14 +1191,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default_ty: Some(_) } ) { - field_with_default_encountered = Some(field_name); + field_with_default_encountered = + Some((field_name, field.single_declaration)); } else if let Some(field_with_default) = field_with_default_encountered.as_ref() { report_namedtuple_field_without_default_after_field_with_default( &self.context, class, - self.index, - &field_name, + &(field_name, field.single_declaration), field_with_default, ); } diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index b3fe37f867..c1b241093b 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -1,6 +1,9 @@ use bitflags::bitflags; +use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; +use ruff_db::parsed::parsed_module; use ruff_python_ast::Arguments; use ruff_python_ast::{self as ast, AnyNodeRef, StmtClassDef, name::Name}; +use ruff_text_size::Ranged; use super::class::{ClassType, CodeGeneratorKind, Field}; use super::context::InferContext; @@ -157,6 +160,22 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>( return false; }; + let add_item_definition_subdiagnostic = |diagnostic: &mut Diagnostic, message| { + if let Some(declaration) = item.single_declaration { + let file = declaration.file(db); + let module = parsed_module(db, file).load(db); + + let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Item declaration"); + sub.annotate( + Annotation::secondary( + Span::from(file).with_range(declaration.full_range(db, &module).range()), + ) + .message(message), + ); + diagnostic.sub(sub); + } + }; + if assignment_kind.is_subscript() && item.is_read_only() { if let Some(builder) = context.report_lint(assignment_kind.diagnostic_type(), key_node.into()) @@ -175,6 +194,8 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>( .secondary(typed_dict_node.into()) .message(format_args!("TypedDict `{typed_dict_d}`")), ); + + add_item_definition_subdiagnostic(&mut diagnostic, "Read-only item declared here"); } return false; @@ -211,6 +232,8 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>( .secondary(key_node.into()) .message(format_args!("key has declared type `{item_type_d}`")), ); + + add_item_definition_subdiagnostic(&mut diagnostic, "Item declared here"); } false From 7ee863b6d7df2757a16dbbff59ca5a04b993d57a Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 5 Sep 2025 13:53:48 +0200 Subject: [PATCH 08/11] [ty] Include `python` folder in `environment.root` if it exists (#20263) ## Summary I felt it was safer to add the `python` folder *in addition* to a possibly-existing `src` folder, even though the `src` folder only contains Rust code for `maturin`-based projects. There might be non-maturin projects where a `python` folder exists for other reasons, next to a normal `src` layout. closes https://github.com/astral-sh/ty/issues/1120 ## Test Plan Tested locally on the egglog-python project. --- crates/ty/docs/configuration.md | 2 +- crates/ty/tests/cli/python_environment.rs | 123 ++++++++++++++++++++++ crates/ty_project/src/metadata/options.rs | 20 +++- ty.schema.json | 2 +- 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md index 946c814a5d..d7debe12a3 100644 --- a/crates/ty/docs/configuration.md +++ b/crates/ty/docs/configuration.md @@ -144,7 +144,7 @@ If left unspecified, ty will try to detect common project layouts and initialize * if a `.//` directory exists, include `.` and `./` in the first party search path * otherwise, default to `.` (flat layout) -Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), +Besides, if a `./python` or `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` or `__init__.pyi` file), it will also be included in the first party search path. **Default value**: `null` diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index d20a9c8add..26b7a1ba4e 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -1752,3 +1752,126 @@ fn default_root_tests_package() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn default_root_python_folder() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("src/foo.py", "foo = 10"), + ("python/bar.py", "bar = 20"), + ( + "python/test_bar.py", + r#" + from foo import foo + from bar import bar + + print(f"{foo} {bar}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// If `python/__init__.py` is present, it is considered a package and `python` is not added to search paths. +#[test] +fn default_root_python_package() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("src/foo.py", "foo = 10"), + ("python/__init__.py", ""), + ("python/bar.py", "bar = 20"), + ( + "python/test_bar.py", + r#" + from foo import foo + from bar import bar # expected unresolved import + + print(f"{foo} {bar}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `bar` + --> python/test_bar.py:3:6 + | + 2 | from foo import foo + 3 | from bar import bar # expected unresolved import + | ^^^ + 4 | + 5 | print(f"{foo} {bar}") + | + info: Searched in the following paths during module resolution: + info: 1. / (first-party code) + info: 2. /src (first-party code) + info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +/// Similarly, if `python/__init__.pyi` is present, it is considered a package and `python` is not added to search paths. +#[test] +fn default_root_python_package_pyi() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("src/foo.py", "foo = 10"), + ("python/__init__.pyi", ""), + ("python/bar.py", "bar = 20"), + ( + "python/test_bar.py", + r#" + from foo import foo + from bar import bar # expected unresolved import + + print(f"{foo} {bar}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `bar` + --> python/test_bar.py:3:6 + | + 2 | from foo import foo + 3 | from bar import bar # expected unresolved import + | ^^^ + 4 | + 5 | print(f"{foo} {bar}") + | + info: Searched in the following paths during module resolution: + info: 1. / (first-party code) + info: 2. /src (first-party code) + info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 1ec0bb2edb..d0770248fa 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -259,11 +259,29 @@ impl Options { vec![project_root.to_path_buf()] }; + let python = project_root.join("python"); + if system.is_directory(&python) + && !system.is_file(&python.join("__init__.py")) + && !system.is_file(&python.join("__init__.pyi")) + && !roots.contains(&python) + { + // If a `./python` directory exists, include it as a source root. This is the recommended layout + // for maturin-based rust/python projects [1]. + // + // https://github.com/PyO3/maturin/blob/979fe1db42bb9e58bc150fa6fc45360b377288bf/README.md?plain=1#L88-L99 + tracing::debug!( + "Including `./python` in `environment.root` because a `./python` directory exists" + ); + + roots.push(python); + } + // Considering pytest test discovery conventions, // we also include the `tests` directory if it exists and is not a package. let tests_dir = project_root.join("tests"); if system.is_directory(&tests_dir) && !system.is_file(&tests_dir.join("__init__.py")) + && !system.is_file(&tests_dir.join("__init__.pyi")) && !roots.contains(&tests_dir) { // If the `tests` directory exists and is not a package, include it as a source root. @@ -428,7 +446,7 @@ pub struct EnvironmentOptions { /// * if a `.//` directory exists, include `.` and `./` in the first party search path /// * otherwise, default to `.` (flat layout) /// - /// Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), + /// Besides, if a `./python` or `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` or `__init__.pyi` file), /// it will also be included in the first party search path. #[serde(skip_serializing_if = "Option::is_none")] #[option( diff --git a/ty.schema.json b/ty.schema.json index 45cc92b023..3aee321617 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -101,7 +101,7 @@ ] }, "root": { - "description": "The root paths of the project, used for finding first-party modules.\n\nAccepts a list of directory paths searched in priority order (first has highest priority).\n\nIf left unspecified, ty will try to detect common project layouts and initialize `root` accordingly:\n\n* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) * if a `.//` directory exists, include `.` and `./` in the first party search path * otherwise, default to `.` (flat layout)\n\nBesides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), it will also be included in the first party search path.", + "description": "The root paths of the project, used for finding first-party modules.\n\nAccepts a list of directory paths searched in priority order (first has highest priority).\n\nIf left unspecified, ty will try to detect common project layouts and initialize `root` accordingly:\n\n* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) * if a `.//` directory exists, include `.` and `./` in the first party search path * otherwise, default to `.` (flat layout)\n\nBesides, if a `./python` or `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` or `__init__.pyi` file), it will also be included in the first party search path.", "type": [ "array", "null" From fdfb51b59593d117498b6bf7a9f636cfb73f93d2 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 5 Sep 2025 14:17:07 +0200 Subject: [PATCH 09/11] [ty] Update mypy_primer, add egglog-python project (#20078) Now that https://github.com/astral-sh/ruff/pull/20263 is merged, we can update mypy_primer and add the new `egglog-python` project to `good.txt`. The ecosystem-analyzer run shows that we now add 1,356 diagnostics (where we had over 5,000 previously, due to the unsupported project layout). --- crates/ty_python_semantic/resources/primer/good.txt | 1 + scripts/mypy_primer.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt index 2c1694edd9..cfef1c11d4 100644 --- a/crates/ty_python_semantic/resources/primer/good.txt +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -39,6 +39,7 @@ django-stubs downforeveryone dragonchain dulwich +egglog-python flake8 flake8-pyi freqtrade diff --git a/scripts/mypy_primer.sh b/scripts/mypy_primer.sh index 05b9bc5ac7..df0648d90c 100755 --- a/scripts/mypy_primer.sh +++ b/scripts/mypy_primer.sh @@ -20,7 +20,7 @@ cd .. echo "Project selector: ${PRIMER_SELECTOR}" # Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs uvx \ - --from="git+https://github.com/hauntsaninja/mypy_primer@a3798a3d7b8470603e650179b0f82deb2154364d" \ + --from="git+https://github.com/hauntsaninja/mypy_primer@830b80cb00dc8ffee20a7ddcad8d6a13b09c18ed" \ mypy_primer \ --repo ruff \ --type-checker ty \ From eb6154f7927ba831f06c7adae1cb4c28f8b5ea4f Mon Sep 17 00:00:00 2001 From: Eric Mark Martin Date: Fri, 5 Sep 2025 10:03:10 -0400 Subject: [PATCH 10/11] [ty] add doc-comments for some variance stuff (#20189) --- crates/ty_python_semantic/src/types.rs | 9 ++++++++ .../ty_python_semantic/src/types/variance.rs | 23 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index bdb74d90ea..82f81659c7 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6551,6 +6551,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> { } Type::GenericAlias(generic_alias) => generic_alias.variance_of(db, typevar), Type::Callable(callable_type) => callable_type.signatures(db).variance_of(db, typevar), + // A type variable is always covariant in itself. Type::TypeVar(other_typevar) | Type::NonInferableTypeVar(other_typevar) if other_typevar == typevar => { @@ -6560,11 +6561,19 @@ impl<'db> VarianceInferable<'db> for Type<'db> { Type::ProtocolInstance(protocol_instance_type) => { protocol_instance_type.variance_of(db, typevar) } + // unions are covariant in their disjuncts Type::Union(union_type) => union_type .elements(db) .iter() .map(|ty| ty.variance_of(db, typevar)) .collect(), + + // Products are covariant in their conjuncts. For negative + // conjuncts, they're contravariant. To see this, suppose we have + // `B` a subtype of `A`. A value of type `~B` could be some non-`B` + // `A`, and so is not assignable to `~A`. On the other hand, a value + // of type `~A` excludes all `A`s, and thus all `B`s, and so _is_ + // assignable to `~B`. Type::Intersection(intersection_type) => intersection_type .positive(db) .iter() diff --git a/crates/ty_python_semantic/src/types/variance.rs b/crates/ty_python_semantic/src/types/variance.rs index 63d250db56..fb9c87d062 100644 --- a/crates/ty_python_semantic/src/types/variance.rs +++ b/crates/ty_python_semantic/src/types/variance.rs @@ -108,9 +108,30 @@ impl std::iter::FromIterator for TypeVarVariance { } pub(crate) trait VarianceInferable<'db>: Sized { + /// The variance of `typevar` in `self` + /// + /// Generally, one will implement this by traversing any types within `self` + /// in which `typevar` could occur, and calling `variance_of` recursively on + /// them. + /// + /// Sometimes the recursive calls will be in positions where you need to + /// specify a non-covariant polarity. See `with_polarity` for more details. fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance; - fn with_polarity(self, polarity: TypeVarVariance) -> WithPolarity { + /// Creates a `VarianceInferable` that applies `polarity` (see + /// `TypeVarVariance::compose`) to the result of variance inference on the + /// underlying value. + /// + /// In some cases, we need to apply a polarity to the recursive call. + /// You can do this with `ty.with_polarity(polarity).variance_of(typevar)`. + /// Generally, this will be whenever the type occurs in argument-position, + /// in which case you will want `TypeVarVariance::Contravariant`, or + /// `TypeVarVariance::Invariant` if the value(s) being annotated is known to + /// be mutable, such as `T` in `list[T]`. See the [typing spec][typing-spec] + /// for more details. + /// + /// [typing-spec]: https://typing.python.org/en/latest/spec/generics.html#variance + fn with_polarity(self, polarity: TypeVarVariance) -> impl VarianceInferable<'db> { WithPolarity { variance_inferable: self, polarity, From 5d52902e186cea6fa73dac1ceebf53208f6e08ba Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 5 Sep 2025 17:56:06 +0100 Subject: [PATCH 11/11] [ty] Implement the legacy PEP-484 convention for indicating positional-only parameters (#20248) Co-authored-by: Carl Meyer --- .../rules/pre_pep570_positional_argument.rs | 15 +-- crates/ruff_python_ast/src/nodes.rs | 9 +- .../resources/mdtest/call/function.md | 74 ++++++++++- .../resources/mdtest/protocols.md | 43 +++++-- crates/ty_python_semantic/src/ast_node_ref.rs | 6 + crates/ty_python_semantic/src/node_key.rs | 6 + .../ty_python_semantic/src/semantic_index.rs | 36 +++++- .../src/semantic_index/builder.rs | 2 +- .../src/semantic_index/definition.rs | 9 ++ .../src/semantic_index/scope.rs | 56 +++----- crates/ty_python_semantic/src/types.rs | 10 +- crates/ty_python_semantic/src/types/class.rs | 46 +++---- .../ty_python_semantic/src/types/context.rs | 2 +- .../ty_python_semantic/src/types/display.rs | 8 +- .../ty_python_semantic/src/types/function.rs | 120 +++++++++++++++--- crates/ty_python_semantic/src/types/infer.rs | 32 +---- .../src/types/signatures.rs | 52 ++++++-- 17 files changed, 376 insertions(+), 150 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs index ec5fd871bb..e481a82dc2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs @@ -1,6 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast as ast; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::{self as ast, ParameterWithDefault}; use ruff_python_semantic::analyze::function_type; use crate::Violation; @@ -85,16 +85,9 @@ pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast function_type::FunctionType::Method | function_type::FunctionType::ClassMethod )); - if let Some(arg) = function_def.parameters.args.get(skip) { - if is_old_style_positional_only(arg) { - checker.report_diagnostic(Pep484StylePositionalOnlyParameter, arg.identifier()); + if let Some(param) = function_def.parameters.args.get(skip) { + if param.uses_pep_484_positional_only_convention() { + checker.report_diagnostic(Pep484StylePositionalOnlyParameter, param.identifier()); } } } - -/// Returns `true` if the [`ParameterWithDefault`] is an old-style positional-only parameter (i.e., -/// its name starts with `__` and does not end with `__`). -fn is_old_style_positional_only(param: &ParameterWithDefault) -> bool { - let arg_name = param.name(); - arg_name.starts_with("__") && !arg_name.ends_with("__") -} diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 6a824603bb..27cfc06da7 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -3219,7 +3219,6 @@ impl<'a> IntoIterator for &'a Box { /// Used by `Arguments` original type. /// /// NOTE: This type is different from original Python AST. - #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ParameterWithDefault { @@ -3241,6 +3240,14 @@ impl ParameterWithDefault { pub fn annotation(&self) -> Option<&Expr> { self.parameter.annotation() } + + /// Return `true` if the parameter name uses the pre-PEP-570 convention + /// (specified in PEP 484) to indicate to a type checker that it should be treated + /// as positional-only. + pub fn uses_pep_484_positional_only_convention(&self) -> bool { + let name = self.name(); + name.starts_with("__") && !name.ends_with("__") + } } /// An AST node used to represent the arguments passed to a function call or class definition. diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 6be6a8d9ec..852623a4f4 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -68,6 +68,78 @@ def _(flag: bool): reveal_type(foo()) # revealed: int ``` +## PEP-484 convention for positional-only parameters + +PEP 570, introduced in Python 3.8, added dedicated Python syntax for denoting positional-only +parameters (the `/` in a function signature). However, functions implemented in C were able to have +positional-only parameters prior to Python 3.8 (there was just no syntax for expressing this at the +Python level). + +Stub files describing functions implemented in C nonetheless needed a way of expressing that certain +parameters were positional-only. In the absence of dedicated Python syntax, PEP 484 described a +convention that type checkers were expected to understand: + +> Some functions are designed to take their arguments only positionally, and expect their callers +> never to use the argument’s name to provide that argument by keyword. All arguments with names +> beginning with `__` are assumed to be positional-only, except if their names also end with `__`. + +While this convention is now redundant (following the implementation of PEP 570), many projects +still continue to use the old convention, so it is supported by ty as well. + +```py +def f(__x: int): ... + +f(1) +# error: [missing-argument] +# error: [unknown-argument] +f(__x=1) +``` + +But not if they follow a non-positional-only parameter: + +```py +def g(x: int, __y: str): ... + +g(x=1, __y="foo") +``` + +And also not if they both start and end with `__`: + +```py +def h(__x__: str): ... + +h(__x__="foo") +``` + +And if *any* parameters use the new PEP-570 convention, the old convention does not apply: + +```py +def i(x: str, /, __y: int): ... + +i("foo", __y=42) # fine +``` + +And `self`/`cls` are implicitly positional-only: + +```py +class C: + def method(self, __x: int): ... + @classmethod + def class_method(cls, __x: str): ... + # (the name of the first parameter is irrelevant; + # a staticmethod works the same as a free function in the global scope) + @staticmethod + def static_method(self, __x: int): ... + +# error: [missing-argument] +# error: [unknown-argument] +C().method(__x=1) +# error: [missing-argument] +# error: [unknown-argument] +C.class_method(__x="1") +C.static_method("x", __x=42) # fine +``` + ## Splatted arguments ### Unknown argument length @@ -545,7 +617,7 @@ def _(args: str) -> None: This is a regression that was highlighted by the ecosystem check, which shows that we might need to rethink how we perform argument expansion during overload resolution. In particular, we might need -to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry +to retry both `match_parameters` *and* `check_types` for each expansion. Currently we only retry `check_types`. The issue is that argument expansion might produce a splatted value with a different arity than what diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 7e4e710f09..1267bb27fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -413,13 +413,13 @@ To see the kinds and types of the protocol members, you can use the debugging ai from ty_extensions import reveal_protocol_interface from typing import SupportsIndex, SupportsAbs, ClassVar, Iterator -# revealed: {"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }} +# revealed: {"method_member": MethodMember(`(self, /) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self, /) -> str` }, "z": PropertyMember { getter: `def z(self, /) -> int`, setter: `def z(self, /, z: int) -> None` }} reveal_protocol_interface(Foo) -# revealed: {"__index__": MethodMember(`(self) -> int`)} +# revealed: {"__index__": MethodMember(`(self, /) -> int`)} reveal_protocol_interface(SupportsIndex) -# revealed: {"__abs__": MethodMember(`(self) -> Unknown`)} +# revealed: {"__abs__": MethodMember(`(self, /) -> Unknown`)} reveal_protocol_interface(SupportsAbs) -# revealed: {"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)} +# revealed: {"__iter__": MethodMember(`(self, /) -> Iterator[Unknown]`), "__next__": MethodMember(`(self, /) -> Unknown`)} reveal_protocol_interface(Iterator) # error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`" @@ -439,9 +439,9 @@ do not implement any special handling for generic aliases passed to the function reveal_type(get_protocol_members(SupportsAbs[int])) # revealed: frozenset[str] reveal_type(get_protocol_members(Iterator[int])) # revealed: frozenset[str] -# revealed: {"__abs__": MethodMember(`(self) -> int`)} +# revealed: {"__abs__": MethodMember(`(self, /) -> int`)} reveal_protocol_interface(SupportsAbs[int]) -# revealed: {"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)} +# revealed: {"__iter__": MethodMember(`(self, /) -> Iterator[int]`), "__next__": MethodMember(`(self, /) -> int`)} reveal_protocol_interface(Iterator[int]) class BaseProto(Protocol): @@ -450,10 +450,10 @@ class BaseProto(Protocol): class SubProto(BaseProto, Protocol): def member(self) -> bool: ... -# revealed: {"member": MethodMember(`(self) -> int`)} +# revealed: {"member": MethodMember(`(self, /) -> int`)} reveal_protocol_interface(BaseProto) -# revealed: {"member": MethodMember(`(self) -> bool`)} +# revealed: {"member": MethodMember(`(self, /) -> bool`)} reveal_protocol_interface(SubProto) class ProtoWithClassVar(Protocol): @@ -1767,7 +1767,7 @@ class Foo(Protocol): def method(self) -> str: ... def f(x: Foo): - reveal_type(type(x).method) # revealed: def method(self) -> str + reveal_type(type(x).method) # revealed: def method(self, /) -> str class Bar: def __init__(self): @@ -1776,6 +1776,31 @@ class Bar: f(Bar()) # error: [invalid-argument-type] ``` +Some protocols use the old convention (specified in PEP-484) for denoting positional-only +parameters. This is supported by ty: + +```py +class HasPosOnlyDunders: + def __invert__(self, /) -> "HasPosOnlyDunders": + return self + + def __lt__(self, other, /) -> bool: + return True + +class SupportsLessThan(Protocol): + def __lt__(self, __other) -> bool: ... + +class Invertable(Protocol): + # `self` and `cls` are always implicitly positional-only for methods defined in `Protocol` + # classes, even if no parameters in the method use the PEP-484 convention. + def __invert__(self) -> object: ... + +static_assert(is_assignable_to(HasPosOnlyDunders, SupportsLessThan)) +static_assert(is_assignable_to(HasPosOnlyDunders, Invertable)) +static_assert(is_assignable_to(str, SupportsLessThan)) +static_assert(is_assignable_to(int, Invertable)) +``` + ## Equivalence of protocols with method or property members Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the diff --git a/crates/ty_python_semantic/src/ast_node_ref.rs b/crates/ty_python_semantic/src/ast_node_ref.rs index 9d1dd60433..ed28bc396b 100644 --- a/crates/ty_python_semantic/src/ast_node_ref.rs +++ b/crates/ty_python_semantic/src/ast_node_ref.rs @@ -49,6 +49,12 @@ pub struct AstNodeRef { _node: PhantomData, } +impl AstNodeRef { + pub(crate) fn index(&self) -> NodeIndex { + self.index + } +} + impl AstNodeRef where T: HasNodeIndex + Ranged + PartialEq + Debug, diff --git a/crates/ty_python_semantic/src/node_key.rs b/crates/ty_python_semantic/src/node_key.rs index 18edfe1a04..a93931294b 100644 --- a/crates/ty_python_semantic/src/node_key.rs +++ b/crates/ty_python_semantic/src/node_key.rs @@ -1,5 +1,7 @@ use ruff_python_ast::{HasNodeIndex, NodeIndex}; +use crate::ast_node_ref::AstNodeRef; + /// Compact key for a node for use in a hash map. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] pub(super) struct NodeKey(NodeIndex); @@ -11,4 +13,8 @@ impl NodeKey { { NodeKey(node.node_index().load()) } + + pub(super) fn from_node_ref(node_ref: &AstNodeRef) -> Self { + NodeKey(node_ref.index()) + } } diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index 2c770ee0a6..fc57d0b2f6 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -159,7 +159,6 @@ pub(crate) fn attribute_scopes<'db, 's>( class_body_scope: ScopeId<'db>, ) -> impl Iterator + use<'s, 'db> { let file = class_body_scope.file(db); - let module = parsed_module(db, file).load(db); let index = semantic_index(db, file); let class_scope_id = class_body_scope.file_scope_id(db); @@ -175,7 +174,7 @@ pub(crate) fn attribute_scopes<'db, 's>( (child_scope_id, scope) }; - function_scope.node().as_function(&module)?; + function_scope.node().as_function()?; Some(function_scope_id) }) } @@ -332,6 +331,39 @@ impl<'db> SemanticIndex<'db> { Some(&self.scopes[self.parent_scope_id(scope_id)?]) } + /// Return the [`Definition`] of the class enclosing this method, given the + /// method's body scope, or `None` if it is not a method. + pub(crate) fn class_definition_of_method( + &self, + function_body_scope: FileScopeId, + ) -> Option> { + let current_scope = self.scope(function_body_scope); + if current_scope.kind() != ScopeKind::Function { + return None; + } + let parent_scope_id = current_scope.parent()?; + let parent_scope = self.scope(parent_scope_id); + + let class_scope = match parent_scope.kind() { + ScopeKind::Class => parent_scope, + ScopeKind::TypeParams => { + let class_scope_id = parent_scope.parent()?; + let potentially_class_scope = self.scope(class_scope_id); + + match potentially_class_scope.kind() { + ScopeKind::Class => potentially_class_scope, + _ => return None, + } + } + _ => return None, + }; + + class_scope + .node() + .as_class() + .map(|node_ref| self.expect_single_definition(node_ref)) + } + fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool { self.parent_scope_id(scope_id) .is_none_or(|parent_scope_id| { diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 943715fd11..95bce62545 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2644,7 +2644,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { match scope.kind() { ScopeKind::Class | ScopeKind::Lambda => return false, ScopeKind::Function => { - return scope.node().expect_function(self.module).is_async; + return scope.node().expect_function().node(self.module).is_async; } ScopeKind::Comprehension | ScopeKind::Module diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 0075c0ff41..b06390fa84 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -1227,3 +1227,12 @@ impl From<&ast::TypeParamTypeVarTuple> for DefinitionNodeKey { Self(NodeKey::from_node(value)) } } + +impl From<&AstNodeRef> for DefinitionNodeKey +where + for<'a> &'a T: Into, +{ + fn from(value: &AstNodeRef) -> Self { + Self(NodeKey::from_node_ref(value)) + } +} diff --git a/crates/ty_python_semantic/src/semantic_index/scope.rs b/crates/ty_python_semantic/src/semantic_index/scope.rs index b29e732305..b807232c78 100644 --- a/crates/ty_python_semantic/src/semantic_index/scope.rs +++ b/crates/ty_python_semantic/src/semantic_index/scope.rs @@ -397,52 +397,38 @@ impl NodeWithScopeKind { } } - pub(crate) fn expect_class<'ast>( - &self, - module: &'ast ParsedModuleRef, - ) -> &'ast ast::StmtClassDef { + pub(crate) fn as_class(&self) -> Option<&AstNodeRef> { match self { - Self::Class(class) => class.node(module), - _ => panic!("expected class"), - } - } - - pub(crate) fn as_class<'ast>( - &self, - module: &'ast ParsedModuleRef, - ) -> Option<&'ast ast::StmtClassDef> { - match self { - Self::Class(class) => Some(class.node(module)), + Self::Class(class) => Some(class), _ => None, } } - pub(crate) fn expect_function<'ast>( - &self, - module: &'ast ParsedModuleRef, - ) -> &'ast ast::StmtFunctionDef { - self.as_function(module).expect("expected function") + pub(crate) fn expect_class(&self) -> &AstNodeRef { + self.as_class().expect("expected class") } - pub(crate) fn expect_type_alias<'ast>( - &self, - module: &'ast ParsedModuleRef, - ) -> &'ast ast::StmtTypeAlias { + pub(crate) fn as_function(&self) -> Option<&AstNodeRef> { match self { - Self::TypeAlias(type_alias) => type_alias.node(module), - _ => panic!("expected type alias"), - } - } - - pub(crate) fn as_function<'ast>( - &self, - module: &'ast ParsedModuleRef, - ) -> Option<&'ast ast::StmtFunctionDef> { - match self { - Self::Function(function) => Some(function.node(module)), + Self::Function(function) => Some(function), _ => None, } } + + pub(crate) fn expect_function(&self) -> &AstNodeRef { + self.as_function().expect("expected function") + } + + pub(crate) fn as_type_alias(&self) -> Option<&AstNodeRef> { + match self { + Self::TypeAlias(type_alias) => Some(type_alias), + _ => None, + } + } + + pub(crate) fn expect_type_alias(&self) -> &AstNodeRef { + self.as_type_alias().expect("expected type alias") + } } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 82f81659c7..80a9206c8c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5649,7 +5649,7 @@ impl<'db> Type<'db> { SpecialFormType::TypingSelf => { let module = parsed_module(db, scope_id.file(db)).load(db); let index = semantic_index(db, scope_id.file(db)); - let Some(class) = nearest_enclosing_class(db, index, scope_id, &module) else { + let Some(class) = nearest_enclosing_class(db, index, scope_id) else { return Err(InvalidTypeExpressionError { fallback_type: Type::unknown(), invalid_expressions: smallvec::smallvec_inline![ @@ -9364,9 +9364,7 @@ fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl<'db> PEP695TypeAliasType<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { let scope = self.rhs_scope(db); - let module = parsed_module(db, scope.file(db)).load(db); - let type_alias_stmt_node = scope.node(db).expect_type_alias(&module); - + let type_alias_stmt_node = scope.node(db).expect_type_alias(); semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) } @@ -9374,9 +9372,9 @@ impl<'db> PEP695TypeAliasType<'db> { pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { let scope = self.rhs_scope(db); let module = parsed_module(db, scope.file(db)).load(db); - let type_alias_stmt_node = scope.node(db).expect_type_alias(&module); + let type_alias_stmt_node = scope.node(db).expect_type_alias(); let definition = self.definition(db); - definition_expression_type(db, definition, &type_alias_stmt_node.value) + definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value) } fn normalized_impl(self, _db: &'db dyn Db, _visitor: &NormalizedVisitor<'db>) -> Self { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 501bea8844..ac8b245a63 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1403,7 +1403,7 @@ impl<'db> ClassLiteral<'db> { let scope = self.body_scope(db); let file = scope.file(db); let parsed = parsed_module(db, file).load(db); - let class_def_node = scope.node(db).expect_class(&parsed); + let class_def_node = scope.node(db).expect_class().node(&parsed); class_def_node.type_params.as_ref().map(|type_params| { let index = semantic_index(db, scope.file(db)); let definition = index.expect_single_definition(class_def_node); @@ -1445,14 +1445,13 @@ impl<'db> ClassLiteral<'db> { /// query depends on the AST of another file (bad!). fn node<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef { let scope = self.body_scope(db); - scope.node(db).expect_class(module) + scope.node(db).expect_class().node(module) } pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { let body_scope = self.body_scope(db); - let module = parsed_module(db, body_scope.file(db)).load(db); let index = semantic_index(db, body_scope.file(db)); - index.expect_single_definition(body_scope.node(db).expect_class(&module)) + index.expect_single_definition(body_scope.node(db).expect_class()) } pub(crate) fn apply_specialization( @@ -2870,8 +2869,8 @@ impl<'db> ClassLiteral<'db> { let class_table = place_table(db, class_body_scope); let is_valid_scope = |method_scope: ScopeId<'db>| { - if let Some(method_def) = method_scope.node(db).as_function(&module) { - let method_name = method_def.name.as_str(); + if let Some(method_def) = method_scope.node(db).as_function() { + let method_name = method_def.node(&module).name.as_str(); if let Place::Type(Type::FunctionLiteral(method_type), _) = class_symbol(db, class_body_scope, method_name).place { @@ -2946,20 +2945,22 @@ impl<'db> ClassLiteral<'db> { } // The attribute assignment inherits the reachability of the method which contains it - let is_method_reachable = - if let Some(method_def) = method_scope.node(db).as_function(&module) { - let method = index.expect_single_definition(method_def); - let method_place = class_table.symbol_id(&method_def.name).unwrap(); - class_map - .all_reachable_symbol_bindings(method_place) - .find_map(|bind| { - (bind.binding.is_defined_and(|def| def == method)) - .then(|| class_map.binding_reachability(db, &bind)) - }) - .unwrap_or(Truthiness::AlwaysFalse) - } else { - Truthiness::AlwaysFalse - }; + let is_method_reachable = if let Some(method_def) = method_scope.node(db).as_function() + { + let method = index.expect_single_definition(method_def); + let method_place = class_table + .symbol_id(&method_def.node(&module).name) + .unwrap(); + class_map + .all_reachable_symbol_bindings(method_place) + .find_map(|bind| { + (bind.binding.is_defined_and(|def| def == method)) + .then(|| class_map.binding_reachability(db, &bind)) + }) + .unwrap_or(Truthiness::AlwaysFalse) + } else { + Truthiness::AlwaysFalse + }; if is_method_reachable.is_always_false() { continue; } @@ -3323,7 +3324,7 @@ impl<'db> ClassLiteral<'db> { pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { let class_scope = self.body_scope(db); let module = parsed_module(db, class_scope.file(db)).load(db); - let class_node = class_scope.node(db).expect_class(&module); + let class_node = class_scope.node(db).expect_class().node(&module); let class_name = &class_node.name; TextRange::new( class_name.start(), @@ -4784,8 +4785,7 @@ impl KnownClass { // 2. The first parameter of the current function (typically `self` or `cls`) match overload.parameter_types() { [] => { - let Some(enclosing_class) = - nearest_enclosing_class(db, index, scope, module) + let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) else { BoundSuperError::UnavailableImplicitArguments .report_diagnostic(context, call_expression.into()); diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 2230603d82..d04bbf28e9 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -172,7 +172,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { // Inspect all ancestor function scopes by walking bottom up and infer the function's type. let mut function_scope_tys = index .ancestor_scopes(scope_id) - .filter_map(|(_, scope)| scope.node().as_function(self.module())) + .filter_map(|(_, scope)| scope.node().as_function()) .map(|node| binding_type(self.db, index.expect_single_definition(node))) .filter_map(Type::into_function_literal); diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index a54d0e92f7..65a350b7ca 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -200,15 +200,15 @@ impl ClassDisplay<'_> { match ancestor_scope.kind() { ScopeKind::Class => { - if let Some(class_def) = node.as_class(&module_ast) { - name_parts.push(class_def.name.as_str().to_string()); + if let Some(class_def) = node.as_class() { + name_parts.push(class_def.node(&module_ast).name.as_str().to_string()); } } ScopeKind::Function => { - if let Some(function_def) = node.as_function(&module_ast) { + if let Some(function_def) = node.as_function() { name_parts.push(format!( "", - function_def.name.as_str() + function_def.node(&module_ast).name.as_str() )); } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index c3cb87f134..0a544a1e5f 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -55,7 +55,7 @@ use bitflags::bitflags; use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast, ParameterWithDefault}; use ruff_text_size::Ranged; use crate::module_resolver::{KnownModule, file_to_module}; @@ -63,7 +63,7 @@ use crate::place::{Boundness, Place, place_from_bindings}; use crate::semantic_index::ast_ids::HasScopedUseId; use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::ScopeId; -use crate::semantic_index::semantic_index; +use crate::semantic_index::{FileScopeId, SemanticIndex, semantic_index}; use crate::types::call::{Binding, CallArguments}; use crate::types::constraints::{ConstraintSet, Constraints}; use crate::types::context::InferContext; @@ -80,7 +80,7 @@ use crate::types::{ BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsEquivalentVisitor, KnownClass, NormalizedVisitor, SpecialFormType, Truthiness, Type, - TypeMapping, TypeRelation, UnionBuilder, all_members, walk_type_mapping, + TypeMapping, TypeRelation, UnionBuilder, all_members, binding_type, walk_type_mapping, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -236,6 +236,22 @@ impl<'db> OverloadLiteral<'db> { self.has_known_decorator(db, FunctionDecorators::OVERLOAD) } + /// Returns true if this overload is decorated with `@staticmethod`, or if it is implicitly a + /// staticmethod. + pub(crate) fn is_staticmethod(self, db: &dyn Db) -> bool { + self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || self.name(db) == "__new__" + } + + /// Returns true if this overload is decorated with `@classmethod`, or if it is implicitly a + /// classmethod. + pub(crate) fn is_classmethod(self, db: &dyn Db) -> bool { + self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) + || matches!( + self.name(db).as_str(), + "__init_subclass__" | "__class_getitem__" + ) + } + fn node<'ast>( self, db: &dyn Db, @@ -249,7 +265,7 @@ impl<'db> OverloadLiteral<'db> { the function is defined." ); - self.body_scope(db).node(db).expect_function(module) + self.body_scope(db).node(db).expect_function().node(module) } /// Returns the [`FileRange`] of the function's name. @@ -258,7 +274,8 @@ impl<'db> OverloadLiteral<'db> { self.file(db), self.body_scope(db) .node(db) - .expect_function(module) + .expect_function() + .node(module) .name .range, ) @@ -274,9 +291,8 @@ impl<'db> OverloadLiteral<'db> { /// over-invalidation. fn definition(self, db: &'db dyn Db) -> Definition<'db> { let body_scope = self.body_scope(db); - let module = parsed_module(db, self.file(db)).load(db); let index = semantic_index(db, body_scope.file(db)); - index.expect_single_definition(body_scope.node(db).expect_function(&module)) + index.expect_single_definition(body_scope.node(db).expect_function()) } /// Returns the overload immediately before this one in the AST. Returns `None` if there is no @@ -290,7 +306,8 @@ impl<'db> OverloadLiteral<'db> { let use_id = self .body_scope(db) .node(db) - .expect_function(&module) + .expect_function() + .node(&module) .name .scoped_use_id(db, scope); @@ -325,17 +342,79 @@ impl<'db> OverloadLiteral<'db> { db: &'db dyn Db, inherited_generic_context: Option>, ) -> Signature<'db> { + /// `self` or `cls` can be implicitly positional-only if: + /// - It is a method AND + /// - No parameters in the method use PEP-570 syntax AND + /// - It is not a `@staticmethod` AND + /// - `self`/`cls` is not explicitly positional-only using the PEP-484 convention AND + /// - Either the next parameter after `self`/`cls` uses the PEP-484 convention, + /// or the enclosing class is a `Protocol` class + fn has_implicitly_positional_only_first_param<'db>( + db: &'db dyn Db, + literal: OverloadLiteral<'db>, + node: &ast::StmtFunctionDef, + scope: FileScopeId, + index: &SemanticIndex, + ) -> bool { + let parameters = &node.parameters; + + if !parameters.posonlyargs.is_empty() { + return false; + } + + let Some(first_param) = parameters.args.first() else { + return false; + }; + + if first_param.uses_pep_484_positional_only_convention() { + return false; + } + + if literal.is_staticmethod(db) { + return false; + } + + let Some(class_definition) = index.class_definition_of_method(scope) else { + return false; + }; + + // `self` and `cls` are always positional-only if the next parameter uses the + // PEP-484 convention. + if parameters + .args + .get(1) + .is_some_and(ParameterWithDefault::uses_pep_484_positional_only_convention) + { + return true; + } + + // If there isn't any parameter other than `self`/`cls`, + // or there is but it isn't using the PEP-484 convention, + // then `self`/`cls` are only implicitly positional-only if + // it is a protocol class. + let class_type = binding_type(db, class_definition); + class_type + .to_class_type(db) + .is_some_and(|class| class.is_protocol(db)) + } + let scope = self.body_scope(db); let module = parsed_module(db, self.file(db)).load(db); - let function_stmt_node = scope.node(db).expect_function(&module); + let function_stmt_node = scope.node(db).expect_function().node(&module); let definition = self.definition(db); + let index = semantic_index(db, scope.file(db)); let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| { - let index = semantic_index(db, scope.file(db)); GenericContext::from_type_params(db, index, definition, type_params) }); - - let index = semantic_index(db, scope.file(db)); - let is_generator = scope.file_scope_id(db).is_generator_function(index); + let file_scope_id = scope.file_scope_id(db); + let is_generator = file_scope_id.is_generator_function(index); + let has_implicitly_positional_first_parameter = has_implicitly_positional_only_first_param( + db, + self, + function_stmt_node, + file_scope_id, + index, + ); Signature::from_function( db, @@ -344,6 +423,7 @@ impl<'db> OverloadLiteral<'db> { definition, function_stmt_node, is_generator, + has_implicitly_positional_first_parameter, ) } @@ -356,7 +436,7 @@ impl<'db> OverloadLiteral<'db> { let span = Span::from(function_scope.file(db)); let node = function_scope.node(db); let module = parsed_module(db, self.file(db)).load(db); - let func_def = node.as_function(&module)?; + let func_def = node.as_function()?.node(&module); let range = parameter_index .and_then(|parameter_index| { func_def @@ -376,7 +456,7 @@ impl<'db> OverloadLiteral<'db> { let span = Span::from(function_scope.file(db)); let node = function_scope.node(db); let module = parsed_module(db, self.file(db)).load(db); - let func_def = node.as_function(&module)?; + let func_def = node.as_function()?.node(&module); let return_type_range = func_def.returns.as_ref().map(|returns| returns.range()); let mut signature = func_def.name.range.cover(func_def.parameters.range); if let Some(return_type_range) = return_type_range { @@ -713,17 +793,15 @@ impl<'db> FunctionType<'db> { /// Returns true if this method is decorated with `@classmethod`, or if it is implicitly a /// classmethod. pub(crate) fn is_classmethod(self, db: &'db dyn Db) -> bool { - self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) - || matches!( - self.name(db).as_str(), - "__init_subclass__" | "__class_getitem__" - ) + self.iter_overloads_and_implementation(db) + .any(|overload| overload.is_classmethod(db)) } /// Returns true if this method is decorated with `@staticmethod`, or if it is implicitly a /// static method. pub(crate) fn is_staticmethod(self, db: &'db dyn Db) -> bool { - self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || self.name(db) == "__new__" + self.iter_overloads_and_implementation(db) + .any(|overload| overload.is_staticmethod(db)) } /// If the implementation of this function is deprecated, returns the `@warnings.deprecated`. diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 8dc5379e0e..ae1fd7f409 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -422,12 +422,11 @@ pub(crate) fn nearest_enclosing_class<'db>( db: &'db dyn Db, semantic: &SemanticIndex<'db>, scope: ScopeId, - parsed: &ParsedModuleRef, ) -> Option> { semantic .ancestor_scopes(scope.file_scope_id(db)) .find_map(|(_, ancestor_scope)| { - let class = ancestor_scope.node().as_class(parsed)?; + let class = ancestor_scope.node().as_class()?; let definition = semantic.expect_single_definition(class); infer_definition_types(db, definition) .declaration_type(definition) @@ -2418,29 +2417,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// behaviour to the [`nearest_enclosing_class`] function. fn class_context_of_current_method(&self) -> Option> { let current_scope_id = self.scope().file_scope_id(self.db()); - let current_scope = self.index.scope(current_scope_id); - if current_scope.kind() != ScopeKind::Function { - return None; - } - let parent_scope_id = current_scope.parent()?; - let parent_scope = self.index.scope(parent_scope_id); - - let class_scope = match parent_scope.kind() { - ScopeKind::Class => parent_scope, - ScopeKind::TypeParams => { - let class_scope_id = parent_scope.parent()?; - let potentially_class_scope = self.index.scope(class_scope_id); - - match potentially_class_scope.kind() { - ScopeKind::Class => potentially_class_scope, - _ => return None, - } - } - _ => return None, - }; - - let class_stmt = class_scope.node().as_class(self.module())?; - let class_definition = self.index.expect_single_definition(class_stmt); + let class_definition = self.index.class_definition_of_method(current_scope_id)?; binding_type(self.db(), class_definition).to_class_type(self.db()) } @@ -2453,7 +2430,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !current_scope.kind().is_non_lambda_function() { return None; } - current_scope.node().as_function(self.module()) + current_scope + .node() + .as_function() + .map(|node_ref| node_ref.node(self.module())) } fn function_decorator_types<'a>( diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 029869a0b2..ffd11bf77f 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -12,7 +12,7 @@ use std::{collections::HashMap, slice::Iter}; -use itertools::EitherOrBoth; +use itertools::{EitherOrBoth, Itertools}; use smallvec::{SmallVec, smallvec_inline}; use super::{DynamicType, Type, TypeVarVariance, definition_expression_type}; @@ -352,9 +352,14 @@ impl<'db> Signature<'db> { definition: Definition<'db>, function_node: &ast::StmtFunctionDef, is_generator: bool, + has_implicitly_positional_first_parameter: bool, ) -> Self { - let parameters = - Parameters::from_parameters(db, definition, function_node.parameters.as_ref()); + let parameters = Parameters::from_parameters( + db, + definition, + function_node.parameters.as_ref(), + has_implicitly_positional_first_parameter, + ); let return_ty = function_node.returns.as_ref().map(|returns| { let plain_return_ty = definition_expression_type(db, definition, returns.as_ref()) .apply_type_mapping( @@ -1139,6 +1144,7 @@ impl<'db> Parameters<'db> { db: &'db dyn Db, definition: Definition<'db>, parameters: &ast::Parameters, + has_implicitly_positional_first_parameter: bool, ) -> Self { let ast::Parameters { posonlyargs, @@ -1149,23 +1155,46 @@ impl<'db> Parameters<'db> { range: _, node_index: _, } = parameters; + let default_type = |param: &ast::ParameterWithDefault| { param .default() .map(|default| definition_expression_type(db, definition, default)) }; - let positional_only = posonlyargs.iter().map(|arg| { + + let pos_only_param = |param: &ast::ParameterWithDefault| { Parameter::from_node_and_kind( db, definition, - &arg.parameter, + ¶m.parameter, ParameterKind::PositionalOnly { - name: Some(arg.parameter.name.id.clone()), - default_type: default_type(arg), + name: Some(param.parameter.name.id.clone()), + default_type: default_type(param), }, ) - }); - let positional_or_keyword = args.iter().map(|arg| { + }; + + let mut positional_only: Vec = posonlyargs.iter().map(pos_only_param).collect(); + + let mut pos_or_keyword_iter = args.iter(); + + // If there are no PEP-570 positional-only parameters, check for the legacy PEP-484 convention + // for denoting positional-only parameters (parameters that start with `__` and do not end with `__`) + if positional_only.is_empty() { + let pos_or_keyword_iter = pos_or_keyword_iter.by_ref(); + + if has_implicitly_positional_first_parameter { + positional_only.extend(pos_or_keyword_iter.next().map(pos_only_param)); + } + + positional_only.extend( + pos_or_keyword_iter + .peeking_take_while(|param| param.uses_pep_484_positional_only_convention()) + .map(pos_only_param), + ); + } + + let positional_or_keyword = pos_or_keyword_iter.map(|arg| { Parameter::from_node_and_kind( db, definition, @@ -1176,6 +1205,7 @@ impl<'db> Parameters<'db> { }, ) }); + let variadic = vararg.as_ref().map(|arg| { Parameter::from_node_and_kind( db, @@ -1186,6 +1216,7 @@ impl<'db> Parameters<'db> { }, ) }); + let keyword_only = kwonlyargs.iter().map(|arg| { Parameter::from_node_and_kind( db, @@ -1197,6 +1228,7 @@ impl<'db> Parameters<'db> { }, ) }); + let keywords = kwarg.as_ref().map(|arg| { Parameter::from_node_and_kind( db, @@ -1207,8 +1239,10 @@ impl<'db> Parameters<'db> { }, ) }); + Self::new( positional_only + .into_iter() .chain(positional_or_keyword) .chain(variadic) .chain(keyword_only)