From 5b6169b02de923d1e549357e86f729224626f4aa Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 7 Nov 2024 21:23:06 +0100 Subject: [PATCH 01/16] [red-knot] Minor fix in intersection type comment (#14176) ## Summary Minor fix in intersection type comment introduced in #14138 --- .../src/types/infer.rs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index ed26412c87a01..34f288d9431b5 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3204,18 +3204,32 @@ impl<'db> TypeInferenceBuilder<'db> { // f(T_inter) = f(P1) & f(P2) & ... & f(Pn) // // The reason for this is the following: In general, for any function 'f', the - // set f(A) & f(B) can be *larger than* the set f(A & B). This means that we - // will return a type that is too wide, which is not necessarily problematic. + // set f(A) & f(B) is *larger than or equal to* the set f(A & B). This means + // that we will return a type that is possibly wider than it could be, but + // never wrong. // // However, we do have to leave out the negative contributions. If we were to // add a contribution like ~f(N1), we would potentially infer result types - // that are too narrow, since ~f(A) can be larger than f(~A). + // that are too narrow. // // As an example for this, consider the intersection type `int & ~Literal[1]`. // If 'f' would be the `==`-comparison with 2, we obviously can't tell if that - // answer would be true or false, so we need to return `bool`. However, if we - // compute f(int) & ~f(Literal[1]), we get `bool & ~Literal[False]`, which can - // be simplified to `Literal[True]` -- a type that is too narrow. + // answer would be true or false, so we need to return `bool`. And indeed, we + // we have (glossing over notational details): + // + // f(int & ~1) + // = f({..., -1, 0, 2, 3, ...}) + // = {..., False, False, True, False, ...} + // = bool + // + // On the other hand, if we were to compute + // + // f(int) & ~f(1) + // = bool & ~False + // = True + // + // we would get a result type `Literal[True]` which is too narrow. + // let mut builder = IntersectionBuilder::new(self.db); for pos in intersection.positive(self.db) { let result = match intersection_on { From 4b08d17088dfcbdc0b4e67a13c6a837886459947 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 7 Nov 2024 22:07:27 +0000 Subject: [PATCH 02/16] [red-knot] Add a new `Type::KnownInstanceType` variant (#14155) ## Summary Fixes #14114. I don't think I can really describe the problems with our current architecture (and therefore the motivations for this PR) any better than @carljm did in that issue, so I'll just copy it out here! --- We currently represent "known instances" (e.g. special forms like `typing.Literal`, which are an instance of `typing._SpecialForm`, but need to be handled differently from other instances of `typing._SpecialForm`) as an `InstanceType` with a `known` field that is `Some(...)`. This makes it easy to handle a known instance as if it were a regular instance type (by ignoring the `known` field), and in some cases (e.g. `Type::member`) that is correct and convenient. But in other cases (e.g. `Type::is_equivalent_to`) it is not correct, and we currently have a bug that we would consider the known-instance type of `typing.Literal` as equivalent to the general instance type for `typing._SpecialForm`, and we would fail to consider it a singleton type or a single-valued type (even though it is both.) An instance type with `known.is_some()` is semantically quite different from an instance type with `known.is_none()`. The former is a singleton type that represents exactly one runtime object; the latter is an open type that represents many runtime objects, including instances of unknown subclasses. It is too error-prone to represent these very-different types as a single `Type` variant. We should instead introduce a dedicated `Type::KnownInstance` variant and force ourselves to handle these explicitly in all `Type` variant matches. ## Possible followups There is still a little bit of awkwardness in our current design in some places, in that we first infer the symbol `typing.Literal` as a `_SpecialForm` instance, and then later convert that instance-type into a known-instance-type. We could also use this `KnownInstanceType` enum to account for other special runtime symbols such as `builtins.Ellipsis` or `builtins.NotImplemented`. I think these might be worth pursuing, but I didn't do them here as they didn't seem essential right now, and I wanted to keep the diff relatively minimal. ## Test Plan `cargo test -p red_knot_python_semantic`. New unit tests added for `Type::is_subtype_of`. --------- Co-authored-by: Carl Meyer --- crates/red_knot_python_semantic/src/types.rs | 240 +++++++++--------- .../src/types/builder.rs | 2 +- .../src/types/display.rs | 8 +- .../src/types/infer.rs | 47 ++-- .../red_knot_python_semantic/src/types/mro.rs | 5 +- .../src/types/narrow.rs | 6 +- 6 files changed, 160 insertions(+), 148 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e2b0efc8164fe..e7803c1dc2b94 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -335,6 +335,8 @@ pub enum Type<'db> { SubclassOf(SubclassOfType<'db>), /// The set of Python objects with the given class in their __class__'s method resolution order Instance(InstanceType<'db>), + /// A single Python object that requires special treatment in the type system + KnownInstance(KnownInstanceType), /// The set of objects in any of the types in the union Union(UnionType<'db>), /// The set of objects in all of the types in the intersection @@ -490,22 +492,22 @@ impl<'db> Type<'db> { (_, Type::Unknown | Type::Any | Type::Todo) => false, (Type::Never, _) => true, (_, Type::Never) => false, - (_, Type::Instance(InstanceType { class, .. })) + (_, Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::Object) => { true } - (Type::Instance(InstanceType { class, .. }), _) + (Type::Instance(InstanceType { class }), _) if class.is_known(db, KnownClass::Object) => { false } - (Type::BooleanLiteral(_), Type::Instance(InstanceType { class, .. })) + (Type::BooleanLiteral(_), Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::Bool) => { true } - (Type::IntLiteral(_), Type::Instance(InstanceType { class, .. })) + (Type::IntLiteral(_), Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::Int) => { true @@ -513,9 +515,9 @@ impl<'db> Type<'db> { (Type::StringLiteral(_), Type::LiteralString) => true, ( Type::StringLiteral(_) | Type::LiteralString, - Type::Instance(InstanceType { class, .. }), + Type::Instance(InstanceType { class }), ) if class.is_known(db, KnownClass::Str) => true, - (Type::BytesLiteral(_), Type::Instance(InstanceType { class, .. })) + (Type::BytesLiteral(_), Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::Bytes) => { true @@ -530,7 +532,7 @@ impl<'db> Type<'db> { }, ) } - (Type::ClassLiteral(..), Type::Instance(InstanceType { class, .. })) + (Type::ClassLiteral(..), Type::Instance(InstanceType { class })) if class.is_known(db, KnownClass::Type) => { true @@ -545,7 +547,6 @@ impl<'db> Type<'db> { Type::SubclassOf(SubclassOfType { class: self_class }), Type::Instance(InstanceType { class: target_class, - .. }), ) if self_class .metaclass(db) @@ -603,15 +604,13 @@ impl<'db> Type<'db> { .iter() .all(|&neg_ty| neg_ty.is_disjoint_from(db, ty)) } - ( - Type::Instance(InstanceType { - class: self_class, .. - }), - Type::Instance(InstanceType { - class: target_class, - .. - }), - ) => self_class.is_subclass_of(db, target_class), + (Type::KnownInstance(left), right) => { + left.instance_fallback(db).is_subtype_of(db, right) + } + (left, Type::KnownInstance(right)) => { + left.is_subtype_of(db, right.instance_fallback(db)) + } + (Type::Instance(left), Type::Instance(right)) => left.is_instance_of(db, right.class), // TODO _ => false, } @@ -660,8 +659,8 @@ impl<'db> Type<'db> { self == other || matches!((self, other), ( - Type::Instance(InstanceType { class: self_class, .. }), - Type::Instance(InstanceType { class: target_class, .. }) + Type::Instance(InstanceType { class: self_class }), + Type::Instance(InstanceType { class: target_class }) ) if self_class.is_known(db, KnownClass::NoneType) && target_class.is_known(db, KnownClass::NoneType)) @@ -753,75 +752,68 @@ impl<'db> Type<'db> { // final classes inside `Type::SubclassOf` everywhere. false } + (Type::KnownInstance(left), Type::KnownInstance(right)) => left != right, + (Type::KnownInstance(left), right) => { + left.instance_fallback(db).is_disjoint_from(db, right) + } + (left, Type::KnownInstance(right)) => { + left.is_disjoint_from(db, right.instance_fallback(db)) + } ( - Type::Instance(InstanceType { - class: class_none, .. - }), - Type::Instance(InstanceType { - class: class_other, .. - }), + Type::Instance(InstanceType { class: class_none }), + Type::Instance(InstanceType { class: class_other }), ) | ( - Type::Instance(InstanceType { - class: class_other, .. - }), - Type::Instance(InstanceType { - class: class_none, .. - }), + Type::Instance(InstanceType { class: class_other }), + Type::Instance(InstanceType { class: class_none }), ) if class_none.is_known(db, KnownClass::NoneType) => !matches!( class_other.known(db), Some(KnownClass::NoneType | KnownClass::Object) ), - ( - Type::Instance(InstanceType { - class: class_none, .. - }), - _, - ) - | ( - _, - Type::Instance(InstanceType { - class: class_none, .. - }), - ) if class_none.is_known(db, KnownClass::NoneType) => true, + (Type::Instance(InstanceType { class: class_none }), _) + | (_, Type::Instance(InstanceType { class: class_none })) + if class_none.is_known(db, KnownClass::NoneType) => + { + true + } - (Type::BooleanLiteral(..), Type::Instance(InstanceType { class, .. })) - | (Type::Instance(InstanceType { class, .. }), Type::BooleanLiteral(..)) => !matches!( + (Type::BooleanLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => !matches!( class.known(db), Some(KnownClass::Bool | KnownClass::Int | KnownClass::Object) ), (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, - (Type::IntLiteral(..), Type::Instance(InstanceType { class, .. })) - | (Type::Instance(InstanceType { class, .. }), Type::IntLiteral(..)) => { + (Type::IntLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::IntLiteral(..)) => { !matches!(class.known(db), Some(KnownClass::Int | KnownClass::Object)) } (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, (Type::StringLiteral(..), Type::LiteralString) | (Type::LiteralString, Type::StringLiteral(..)) => false, - (Type::StringLiteral(..), Type::Instance(InstanceType { class, .. })) - | (Type::Instance(InstanceType { class, .. }), Type::StringLiteral(..)) => { + (Type::StringLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::StringLiteral(..)) => { !matches!(class.known(db), Some(KnownClass::Str | KnownClass::Object)) } (Type::StringLiteral(..), _) | (_, Type::StringLiteral(..)) => true, (Type::LiteralString, Type::LiteralString) => false, - (Type::LiteralString, Type::Instance(InstanceType { class, .. })) - | (Type::Instance(InstanceType { class, .. }), Type::LiteralString) => { + (Type::LiteralString, Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::LiteralString) => { !matches!(class.known(db), Some(KnownClass::Str | KnownClass::Object)) } (Type::LiteralString, _) | (_, Type::LiteralString) => true, - (Type::BytesLiteral(..), Type::Instance(InstanceType { class, .. })) - | (Type::Instance(InstanceType { class, .. }), Type::BytesLiteral(..)) => !matches!( + (Type::BytesLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::BytesLiteral(..)) => !matches!( class.known(db), Some(KnownClass::Bytes | KnownClass::Object) ), (Type::BytesLiteral(..), _) | (_, Type::BytesLiteral(..)) => true, - (Type::SliceLiteral(..), Type::Instance(InstanceType { class, .. })) - | (Type::Instance(InstanceType { class, .. }), Type::SliceLiteral(..)) => !matches!( + (Type::SliceLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::SliceLiteral(..)) => !matches!( class.known(db), Some(KnownClass::Slice | KnownClass::Object) ), @@ -829,10 +821,10 @@ impl<'db> Type<'db> { ( Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..), - Type::Instance(InstanceType { class, .. }), + Type::Instance(InstanceType { class }), ) | ( - Type::Instance(InstanceType { class, .. }), + Type::Instance(InstanceType { class }), Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..), ) => !class.is_known(db, KnownClass::Object), @@ -902,8 +894,9 @@ impl<'db> Type<'db> { Type::BooleanLiteral(_) | Type::FunctionLiteral(..) | Type::ClassLiteral(..) - | Type::ModuleLiteral(..) => true, - Type::Instance(InstanceType { class, .. }) => { + | Type::ModuleLiteral(..) + | Type::KnownInstance(..) => true, + Type::Instance(InstanceType { class }) => { // TODO some more instance types can be singleton types (EllipsisType, NotImplementedType) matches!(class.known(db), Some(KnownClass::NoneType)) } @@ -944,7 +937,8 @@ impl<'db> Type<'db> { | Type::BooleanLiteral(..) | Type::StringLiteral(..) | Type::BytesLiteral(..) - | Type::SliceLiteral(..) => true, + | Type::SliceLiteral(..) + | Type::KnownInstance(..) => true, Type::SubclassOf(..) => { // TODO: Same comment as above for `is_singleton` @@ -956,7 +950,7 @@ impl<'db> Type<'db> { .iter() .all(|elem| elem.is_single_valued(db)), - Type::Instance(InstanceType { class, .. }) => match class.known(db) { + Type::Instance(InstanceType { class }) => match class.known(db) { Some(KnownClass::NoneType) => true, Some( KnownClass::Bool @@ -1045,6 +1039,9 @@ impl<'db> Type<'db> { } Type::ClassLiteral(class_ty) => class_ty.member(db, name), Type::SubclassOf(subclass_of_ty) => subclass_of_ty.member(db, name), + Type::KnownInstance(known_instance) => { + known_instance.instance_fallback(db).member(db, name) + } Type::Instance(_) => { // TODO MRO? get_own_instance_member, get_instance_member Type::Todo.into() @@ -1139,7 +1136,7 @@ impl<'db> Type<'db> { // TODO: see above Truthiness::Ambiguous } - Type::Instance(InstanceType { class, .. }) => { + Type::Instance(InstanceType { class }) => { // TODO: lookup `__bool__` and `__len__` methods on the instance's class // More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing // For now, we only special-case some builtin classes @@ -1149,6 +1146,7 @@ impl<'db> Type<'db> { Truthiness::Ambiguous } } + Type::KnownInstance(known_instance) => known_instance.bool(), Type::Union(union) => { let union_elements = union.elements(db); let first_element_truthiness = union_elements[0].bool(db); @@ -1204,11 +1202,11 @@ impl<'db> Type<'db> { .first() .map(|arg| arg.bool(db).into_type(db)) .unwrap_or(Type::BooleanLiteral(false)), - _ => Type::anonymous_instance(class), + _ => Type::Instance(InstanceType { class }), }) } - instance_ty @ Type::Instance(InstanceType { .. }) => { + instance_ty @ Type::Instance(_) => { let args = std::iter::once(self) .chain(arg_types.iter().copied()) .collect::>(); @@ -1355,8 +1353,12 @@ impl<'db> Type<'db> { Type::Todo => Type::Todo, Type::Unknown => Type::Unknown, Type::Never => Type::Never, - Type::ClassLiteral(ClassLiteralType { class }) => Type::anonymous_instance(*class), - Type::SubclassOf(SubclassOfType { class }) => Type::anonymous_instance(*class), + Type::ClassLiteral(ClassLiteralType { class }) => { + Type::Instance(InstanceType { class: *class }) + } + Type::SubclassOf(SubclassOfType { class }) => { + Type::Instance(InstanceType { class: *class }) + } Type::Union(union) => union.map(db, |element| element.to_instance(db)), // TODO: we can probably do better here: --Alex Type::Intersection(_) => Type::Todo, @@ -1366,6 +1368,7 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::FunctionLiteral(_) | Type::Instance(_) + | Type::KnownInstance(_) | Type::ModuleLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) @@ -1375,10 +1378,6 @@ impl<'db> Type<'db> { } } - pub fn anonymous_instance(class: Class<'db>) -> Self { - Self::Instance(InstanceType::anonymous(class)) - } - /// The type `NoneType` / `None` pub fn none(db: &'db dyn Db) -> Type<'db> { KnownClass::NoneType.to_instance(db) @@ -1390,9 +1389,10 @@ impl<'db> Type<'db> { pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::Never => Type::Never, - Type::Instance(InstanceType { class, .. }) => { + Type::Instance(InstanceType { class }) => { Type::SubclassOf(SubclassOfType { class: *class }) } + Type::KnownInstance(known_instance) => known_instance.class().to_class(db), Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), Type::BooleanLiteral(_) => KnownClass::Bool.to_class(db), Type::BytesLiteral(_) => KnownClass::Bytes.to_class(db), @@ -1431,6 +1431,9 @@ impl<'db> Type<'db> { match self { Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db), Type::StringLiteral(_) | Type::LiteralString => *self, + Type::KnownInstance(known_instance) => { + Type::StringLiteral(StringLiteralType::new(db, known_instance.repr())) + } // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), } @@ -1450,6 +1453,9 @@ impl<'db> Type<'db> { format!("'{}'", literal.value(db).escape_default()).into_boxed_str() })), Type::LiteralString => Type::LiteralString, + Type::KnownInstance(known_instance) => { + Type::StringLiteral(StringLiteralType::new(db, known_instance.repr())) + } // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), } @@ -1612,39 +1618,58 @@ impl<'db> KnownClass { } } +/// Enumeration of specific runtime that are special enough to be considered their own type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum KnownInstance { +pub enum KnownInstanceType { + /// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`) Literal, // TODO: fill this enum out with more special forms, etc. } -impl KnownInstance { - pub const fn as_str(&self) -> &'static str { +impl KnownInstanceType { + pub const fn as_str(self) -> &'static str { match self { - KnownInstance::Literal => "Literal", + KnownInstanceType::Literal => "Literal", } } - pub fn maybe_from_module(module: &Module, instance_name: &str) -> Option { - let candidate = Self::from_name(instance_name)?; - candidate.check_module(module).then_some(candidate) + /// Evaluate the known instance in boolean context + pub const fn bool(self) -> Truthiness { + match self { + Self::Literal => Truthiness::AlwaysTrue, + } } - fn from_name(name: &str) -> Option { - match name { - "Literal" => Some(Self::Literal), - _ => None, + /// Return the repr of the symbol at runtime + pub const fn repr(self) -> &'static str { + match self { + Self::Literal => "typing.Literal", } } - fn check_module(self, module: &Module) -> bool { + /// Return the [`KnownClass`] which this symbol is an instance of + pub const fn class(self) -> KnownClass { + match self { + Self::Literal => KnownClass::SpecialForm, + } + } + + /// Return the instance type which this type is a subtype of. + /// + /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`, + /// so `KnownInstanceType::Literal.instance_fallback(db)` + /// returns `Type::Instance(InstanceType { class: })`. + pub fn instance_fallback(self, db: &dyn Db) -> Type { + self.class().to_instance(db) + } + + pub fn try_from_module_and_symbol(module: &Module, instance_name: &str) -> Option { if !module.search_path().is_standard_library() { - return false; + return None; } - match self { - Self::Literal => { - matches!(module.name().as_str(), "typing" | "typing_extensions") - } + match (module.name().as_str(), instance_name) { + ("typing" | "typing_extensions", "Literal") => Some(Self::Literal), + _ => None, } } } @@ -1993,7 +2018,7 @@ impl<'db> IterationOutcome<'db> { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum Truthiness { +pub enum Truthiness { /// For an object `x`, `bool(x)` will always return `True` AlwaysTrue, /// For an object `x`, `bool(x)` will always return `False` @@ -2475,37 +2500,12 @@ impl<'db> SubclassOfType<'db> { } /// A type representing the set of runtime objects which are instances of a certain class. -/// -/// Some specific instances of some types need to be treated specially by the type system: -/// for example, various special forms are instances of `typing._SpecialForm`, -/// but need to be handled differently in annotations. These special instances are marked as such -/// using the `known` field on this struct. -/// -/// Note that, for example, `InstanceType { class: typing._SpecialForm, known: None }` -/// is a supertype of `InstanceType { class: typing._SpecialForm, known: KnownInstance::Literal }`. -/// The two types are not disjoint. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct InstanceType<'db> { class: Class<'db>, - known: Option, } impl<'db> InstanceType<'db> { - pub fn anonymous(class: Class<'db>) -> Self { - Self { class, known: None } - } - - pub fn known(class: Class<'db>, known: KnownInstance) -> Self { - Self { - class, - known: Some(known), - } - } - - pub fn is_known(&self, known_instance: KnownInstance) -> bool { - self.known == Some(known_instance) - } - /// Return `true` if members of this type are instances of the class `class` at runtime. pub fn is_instance_of(self, db: &'db dyn Db, class: Class<'db>) -> bool { self.class.is_subclass_of(db, class) @@ -2703,6 +2703,8 @@ mod tests { BytesLiteral(&'static str), // BuiltinInstance("str") corresponds to an instance of the builtin `str` class BuiltinInstance(&'static str), + TypingInstance(&'static str), + KnownInstance(KnownInstanceType), // BuiltinClassLiteral("str") corresponds to the builtin `str` class object itself BuiltinClassLiteral(&'static str), Union(Vec), @@ -2724,6 +2726,8 @@ mod tests { Ty::LiteralString => Type::LiteralString, Ty::BytesLiteral(s) => Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes())), Ty::BuiltinInstance(s) => builtins_symbol(db, s).expect_type().to_instance(db), + Ty::TypingInstance(s) => typing_symbol(db, s).expect_type().to_instance(db), + Ty::KnownInstance(known_instance) => Type::KnownInstance(known_instance), Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).expect_type(), Ty::Union(tys) => { UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) @@ -2813,6 +2817,14 @@ mod tests { #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("str")], neg: vec![Ty::StringLiteral("foo")]}, Ty::Intersection{pos: vec![], neg: vec![Ty::IntLiteral(2)]})] #[test_case(Ty::BuiltinClassLiteral("int"), Ty::BuiltinClassLiteral("int"))] #[test_case(Ty::BuiltinClassLiteral("int"), Ty::BuiltinInstance("object"))] + #[test_case( + Ty::KnownInstance(KnownInstanceType::Literal), + Ty::TypingInstance("_SpecialForm") + )] + #[test_case( + Ty::KnownInstance(KnownInstanceType::Literal), + Ty::BuiltinInstance("object") + )] fn is_subtype_of(from: Ty, to: Ty) { let db = setup_db(); assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db))); diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 2ff1f9fad1547..295b73bc7ece6 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -246,7 +246,7 @@ impl<'db> InnerIntersectionBuilder<'db> { } } else { // ~Literal[True] & bool = Literal[False] - if let Type::Instance(InstanceType { class, .. }) = new_positive { + if let Type::Instance(InstanceType { class }) = new_positive { if class.is_known(db, KnownClass::Bool) { if let Some(&Type::BooleanLiteral(value)) = self .negative diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 9e0b5dfeede35..6818fe12d6ea2 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -66,7 +66,7 @@ impl Display for DisplayRepresentation<'_> { Type::Any => f.write_str("Any"), Type::Never => f.write_str("Never"), Type::Unknown => f.write_str("Unknown"), - Type::Instance(InstanceType { class, .. }) + Type::Instance(InstanceType { class }) if class.is_known(self.db, KnownClass::NoneType) => { f.write_str("None") @@ -82,10 +82,8 @@ impl Display for DisplayRepresentation<'_> { Type::SubclassOf(SubclassOfType { class }) => { write!(f, "type[{}]", class.name(self.db)) } - Type::Instance(InstanceType { class, known }) => f.write_str(match known { - Some(super::KnownInstance::Literal) => "Literal", - _ => class.name(self.db), - }), + Type::Instance(InstanceType { class }) => f.write_str(class.name(self.db)), + Type::KnownInstance(known_instance) => f.write_str(known_instance.as_str()), Type::FunctionLiteral(function) => f.write_str(function.name(self.db)), Type::Union(union) => union.display(self.db).fmt(f), Type::Intersection(intersection) => intersection.display(self.db).fmt(f), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 34f288d9431b5..c716f6ff0b88f 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -58,7 +58,7 @@ use crate::types::{ bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, typing_extensions_symbol, Boundness, BytesLiteralType, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, - KnownInstance, MetaclassErrorKind, SliceLiteralType, StringLiteralType, Symbol, Truthiness, + KnownInstanceType, MetaclassErrorKind, SliceLiteralType, StringLiteralType, Symbol, Truthiness, TupleType, Type, TypeArrayDisplay, UnionBuilder, UnionType, }; use crate::unpack::Unpack; @@ -634,7 +634,7 @@ impl<'db> TypeInferenceBuilder<'db> { fn check_division_by_zero(&mut self, expr: &ast::ExprBinOp, left: Type<'db>) { match left { Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} - Type::Instance(InstanceType { class, .. }) + Type::Instance(InstanceType { class }) if matches!( class.known(self.db), Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) @@ -1297,13 +1297,15 @@ impl<'db> TypeInferenceBuilder<'db> { // anything else is invalid and should lead to a diagnostic being reported --Alex match node_ty { Type::Any | Type::Unknown => node_ty, - Type::ClassLiteral(ClassLiteralType { class }) => Type::anonymous_instance(class), + Type::ClassLiteral(ClassLiteralType { class }) => { + Type::Instance(InstanceType { class }) + } Type::Tuple(tuple) => UnionType::from_elements( self.db, tuple.elements(self.db).iter().map(|ty| { ty.into_class_literal() .map_or(Type::Todo, |ClassLiteralType { class }| { - Type::anonymous_instance(class) + Type::Instance(InstanceType { class }) }) }), ), @@ -1503,14 +1505,16 @@ impl<'db> TypeInferenceBuilder<'db> { // If the declared variable is annotated with _SpecialForm class then we treat it differently // by assigning the known field to the instance. - if let Type::Instance(InstanceType { class, .. }) = annotation_ty { + if let Type::Instance(InstanceType { class }) = annotation_ty { if class.is_known(self.db, KnownClass::SpecialForm) { if let Some(name_expr) = target.as_name_expr() { - let maybe_known_instance = file_to_module(self.db, self.file) + if let Some(known_instance) = file_to_module(self.db, self.file) .as_ref() - .and_then(|module| KnownInstance::maybe_from_module(module, &name_expr.id)); - if let Some(known_instance) = maybe_known_instance { - annotation_ty = Type::Instance(InstanceType::known(class, known_instance)); + .and_then(|module| { + KnownInstanceType::try_from_module_and_symbol(module, &name_expr.id) + }) + { + annotation_ty = Type::KnownInstance(known_instance); } } } @@ -1555,7 +1559,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_augmented_op(assignment, target_type, value_type) }) } - Type::Instance(InstanceType { class, .. }) => { + Type::Instance(InstanceType { class }) => { if let Symbol::Type(class_member, boundness) = class.class_member(self.db, op.in_place_dunder()) { @@ -2717,7 +2721,7 @@ impl<'db> TypeInferenceBuilder<'db> { (_, Type::Unknown) => Type::Unknown, ( op @ (UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert), - Type::Instance(InstanceType { class, .. }), + Type::Instance(InstanceType { class }), ) => { let unary_dunder_method = match op { UnaryOp::Invert => "__invert__", @@ -3848,7 +3852,7 @@ impl<'db> TypeInferenceBuilder<'db> { Err(_) => SliceArg::Unsupported, }, Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))), - Some(Type::Instance(InstanceType { class, .. })) + Some(Type::Instance(InstanceType { class })) if class.is_known(self.db, KnownClass::NoneType) => { SliceArg::Arg(None) @@ -4194,10 +4198,9 @@ impl<'db> TypeInferenceBuilder<'db> { } = subscript; match value_ty { - Type::Instance(InstanceType { - class: _, - known: Some(known_instance), - }) => self.infer_parameterized_known_instance_type_expression(known_instance, slice), + Type::KnownInstance(known_instance) => { + self.infer_parameterized_known_instance_type_expression(known_instance, slice) + } _ => { self.infer_type_expression(slice); Type::Todo // TODO: generics @@ -4207,11 +4210,11 @@ impl<'db> TypeInferenceBuilder<'db> { fn infer_parameterized_known_instance_type_expression( &mut self, - known_instance: KnownInstance, + known_instance: KnownInstanceType, parameters: &ast::Expr, ) -> Type<'db> { match known_instance { - KnownInstance::Literal => match self.infer_literal_parameter_type(parameters) { + KnownInstanceType::Literal => match self.infer_literal_parameter_type(parameters) { Ok(ty) => ty, Err(nodes) => { for node in nodes { @@ -4238,13 +4241,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO handle type aliases ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { let value_ty = self.infer_expression(value); - if matches!( - value_ty, - Type::Instance(InstanceType { - known: Some(KnownInstance::Literal), - .. - }) - ) { + if matches!(value_ty, Type::KnownInstance(KnownInstanceType::Literal)) { self.infer_literal_parameter_type(slice)? } else { return Err(vec![parameters]); diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index 74e44fe46485e..c30ad49a622af 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -5,7 +5,7 @@ use indexmap::IndexSet; use itertools::Either; use rustc_hash::FxHashSet; -use super::{Class, ClassLiteralType, KnownClass, Type}; +use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, Type}; use crate::Db; /// The inferred method resolution order of a given class. @@ -379,6 +379,9 @@ impl<'db> ClassBase<'db> { | Type::SliceLiteral(_) | Type::ModuleLiteral(_) | Type::SubclassOf(_) => None, + Type::KnownInstance(known_instance) => match known_instance { + KnownInstanceType::Literal => None, + }, } } diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index d28006adedb1d..4c6ee34dad41d 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -5,7 +5,7 @@ use crate::semantic_index::expression::Expression; use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable}; use crate::semantic_index::symbol_table; use crate::types::{ - infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, + infer_expression_types, ClassLiteralType, InstanceType, IntersectionBuilder, KnownClass, KnownConstraintFunction, KnownFunction, Truthiness, Type, UnionBuilder, }; use crate::Db; @@ -353,7 +353,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> { let to_constraint = match function { KnownConstraintFunction::IsInstance => { |class_literal: ClassLiteralType<'db>| { - Type::anonymous_instance(class_literal.class) + Type::Instance(InstanceType { + class: class_literal.class, + }) } } KnownConstraintFunction::IsSubclass => { From 2624249219af9c029511fec36669430b9b124922 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 7 Nov 2024 23:23:35 +0100 Subject: [PATCH 03/16] [red-knot] Minor: fix `Literal[True] <: int` (#14177) ## Summary Minor fix to `Type::is_subtype_of` to make sure that Boolean literals are subtypes of `int`, to match runtime semantics. Found this while doing some property-testing experiments [1]. [1] https://github.com/astral-sh/ruff/pull/14178 ## Test Plan New unit test. --- crates/red_knot_python_semantic/src/types.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e7803c1dc2b94..cae012ed95dc9 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -503,7 +503,7 @@ impl<'db> Type<'db> { false } (Type::BooleanLiteral(_), Type::Instance(InstanceType { class })) - if class.is_known(db, KnownClass::Bool) => + if matches!(class.known(db), Some(KnownClass::Bool | KnownClass::Int)) => { true } @@ -2789,6 +2789,7 @@ mod tests { #[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("int"))] #[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("object"))] #[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("bool"))] + #[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("int"))] #[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("object"))] #[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("str"))] #[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("object"))] From 272d24bf3e4dabe59d2ec49f505e8fa4e00ba798 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 7 Nov 2024 22:45:19 -0500 Subject: [PATCH 04/16] [`flake8-pyi`] Add a fix for `duplicate-literal-member` (#14188) ## Summary Closes https://github.com/astral-sh/ruff/issues/14187. --- .../rules/duplicate_literal_member.rs | 54 +++++- ...__flake8_pyi__tests__PYI062_PYI062.py.snap | 181 ++++++++++++++++-- ..._flake8_pyi__tests__PYI062_PYI062.pyi.snap | 181 ++++++++++++++++-- 3 files changed, 380 insertions(+), 36 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs index 4c3a8001dc6d6..c3c1d7225b759 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs @@ -2,12 +2,12 @@ use std::collections::HashSet; use rustc_hash::FxHashSet; -use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::Expr; +use ruff_python_ast::{self as ast, Expr, ExprContext}; use ruff_python_semantic::analyze::typing::traverse_literal; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -27,6 +27,10 @@ use crate::checkers::ast::Checker; /// foo: Literal["a", "b"] /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as safe; however, the fix will flatten nested +/// literals into a single top-level literal. +/// /// ## References /// - [Python documentation: `typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal) #[violation] @@ -34,24 +38,29 @@ pub struct DuplicateLiteralMember { duplicate_name: String, } -impl Violation for DuplicateLiteralMember { - const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; - +impl AlwaysFixableViolation for DuplicateLiteralMember { #[derive_message_formats] fn message(&self) -> String { format!("Duplicate literal member `{}`", self.duplicate_name) } + + fn fix_title(&self) -> String { + "Remove duplicates".to_string() + } } /// PYI062 pub(crate) fn duplicate_literal_member<'a>(checker: &mut Checker, expr: &'a Expr) { let mut seen_nodes: HashSet, _> = FxHashSet::default(); + let mut unique_nodes: Vec<&Expr> = Vec::new(); let mut diagnostics: Vec = Vec::new(); // Adds a member to `literal_exprs` if it is a `Literal` annotation let mut check_for_duplicate_members = |expr: &'a Expr, _: &'a Expr| { // If we've already seen this literal member, raise a violation. - if !seen_nodes.insert(expr.into()) { + if seen_nodes.insert(expr.into()) { + unique_nodes.push(expr); + } else { diagnostics.push(Diagnostic::new( DuplicateLiteralMember { duplicate_name: checker.generator().expr(expr), @@ -61,7 +70,36 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &mut Checker, expr: &'a Expr } }; - // Traverse the literal, collect all diagnostic members + // Traverse the literal, collect all diagnostic members. traverse_literal(&mut check_for_duplicate_members, checker.semantic(), expr); + + // If there's at least one diagnostic, create a fix to remove the duplicate members. + if !diagnostics.is_empty() { + if let Expr::Subscript(subscript) = expr { + let subscript = Expr::Subscript(ast::ExprSubscript { + slice: Box::new(if let [elt] = unique_nodes.as_slice() { + (*elt).clone() + } else { + Expr::Tuple(ast::ExprTuple { + elts: unique_nodes.into_iter().cloned().collect(), + range: TextRange::default(), + ctx: ExprContext::Load, + parenthesized: false, + }) + }), + value: subscript.value.clone(), + range: TextRange::default(), + ctx: ExprContext::Load, + }); + let fix = Fix::safe_edit(Edit::range_replacement( + checker.generator().expr(&subscript), + expr.range(), + )); + for diagnostic in &mut diagnostics { + diagnostic.set_fix(fix.clone()); + } + } + } + checker.diagnostics.append(&mut diagnostics); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap index 2ed8215957160..c73ae4f46e55b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs --- -PYI062.py:5:25: PYI062 Duplicate literal member `True` +PYI062.py:5:25: PYI062 [*] Duplicate literal member `True` | 3 | import typing_extensions 4 | @@ -10,8 +10,19 @@ PYI062.py:5:25: PYI062 Duplicate literal member `True` 6 | 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1 | + = help: Remove duplicates -PYI062.py:5:31: PYI062 Duplicate literal member `False` +ℹ Safe fix +2 2 | import typing as t +3 3 | import typing_extensions +4 4 | +5 |-x: Literal[True, False, True, False] # PYI062 twice here + 5 |+x: Literal[True, False] # PYI062 twice here +6 6 | +7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1 +8 8 | + +PYI062.py:5:31: PYI062 [*] Duplicate literal member `False` | 3 | import typing_extensions 4 | @@ -20,8 +31,19 @@ PYI062.py:5:31: PYI062 Duplicate literal member `False` 6 | 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1 | + = help: Remove duplicates + +ℹ Safe fix +2 2 | import typing as t +3 3 | import typing_extensions +4 4 | +5 |-x: Literal[True, False, True, False] # PYI062 twice here + 5 |+x: Literal[True, False] # PYI062 twice here +6 6 | +7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1 +8 8 | -PYI062.py:7:45: PYI062 Duplicate literal member `1` +PYI062.py:7:45: PYI062 [*] Duplicate literal member `1` | 5 | x: Literal[True, False, True, False] # PYI062 twice here 6 | @@ -30,8 +52,19 @@ PYI062.py:7:45: PYI062 Duplicate literal member `1` 8 | 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal | + = help: Remove duplicates -PYI062.py:9:33: PYI062 Duplicate literal member `{1, 3, 5}` +ℹ Safe fix +4 4 | +5 5 | x: Literal[True, False, True, False] # PYI062 twice here +6 6 | +7 |-y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1 + 7 |+y: Literal[1, print("hello"), 3, 4] # PYI062 on the last 1 +8 8 | +9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal +10 10 | + +PYI062.py:9:33: PYI062 [*] Duplicate literal member `{1, 3, 5}` | 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1 8 | @@ -40,8 +73,19 @@ PYI062.py:9:33: PYI062 Duplicate literal member `{1, 3, 5}` 10 | 11 | Literal[1, Literal[1]] # once | + = help: Remove duplicates + +ℹ Safe fix +6 6 | +7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1 +8 8 | +9 |-z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal + 9 |+z: Literal[{1, 3, 5}, "foobar"] # PYI062 on the set literal +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice -PYI062.py:11:20: PYI062 Duplicate literal member `1` +PYI062.py:11:20: PYI062 [*] Duplicate literal member `1` | 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal 10 | @@ -50,8 +94,19 @@ PYI062.py:11:20: PYI062 Duplicate literal member `1` 12 | Literal[1, 2, Literal[1, 2]] # twice 13 | Literal[1, Literal[1], Literal[1]] # twice | + = help: Remove duplicates -PYI062.py:12:23: PYI062 Duplicate literal member `1` +ℹ Safe fix +8 8 | +9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal +10 10 | +11 |-Literal[1, Literal[1]] # once + 11 |+Literal[1] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once + +PYI062.py:12:23: PYI062 [*] Duplicate literal member `1` | 11 | Literal[1, Literal[1]] # once 12 | Literal[1, 2, Literal[1, 2]] # twice @@ -59,8 +114,19 @@ PYI062.py:12:23: PYI062 Duplicate literal member `1` 13 | Literal[1, Literal[1], Literal[1]] # twice 14 | Literal[1, Literal[2], Literal[2]] # once | + = help: Remove duplicates + +ℹ Safe fix +9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 |-Literal[1, 2, Literal[1, 2]] # twice + 12 |+Literal[1, 2] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once -PYI062.py:12:26: PYI062 Duplicate literal member `2` +PYI062.py:12:26: PYI062 [*] Duplicate literal member `2` | 11 | Literal[1, Literal[1]] # once 12 | Literal[1, 2, Literal[1, 2]] # twice @@ -68,8 +134,19 @@ PYI062.py:12:26: PYI062 Duplicate literal member `2` 13 | Literal[1, Literal[1], Literal[1]] # twice 14 | Literal[1, Literal[2], Literal[2]] # once | + = help: Remove duplicates + +ℹ Safe fix +9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 |-Literal[1, 2, Literal[1, 2]] # twice + 12 |+Literal[1, 2] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once -PYI062.py:13:20: PYI062 Duplicate literal member `1` +PYI062.py:13:20: PYI062 [*] Duplicate literal member `1` | 11 | Literal[1, Literal[1]] # once 12 | Literal[1, 2, Literal[1, 2]] # twice @@ -78,8 +155,19 @@ PYI062.py:13:20: PYI062 Duplicate literal member `1` 14 | Literal[1, Literal[2], Literal[2]] # once 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once | + = help: Remove duplicates -PYI062.py:13:32: PYI062 Duplicate literal member `1` +ℹ Safe fix +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 |-Literal[1, Literal[1], Literal[1]] # twice + 13 |+Literal[1] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 16 | typing_extensions.Literal[1, 1, 1] # twice + +PYI062.py:13:32: PYI062 [*] Duplicate literal member `1` | 11 | Literal[1, Literal[1]] # once 12 | Literal[1, 2, Literal[1, 2]] # twice @@ -88,8 +176,19 @@ PYI062.py:13:32: PYI062 Duplicate literal member `1` 14 | Literal[1, Literal[2], Literal[2]] # once 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once | + = help: Remove duplicates + +ℹ Safe fix +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 |-Literal[1, Literal[1], Literal[1]] # twice + 13 |+Literal[1] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 16 | typing_extensions.Literal[1, 1, 1] # twice -PYI062.py:14:32: PYI062 Duplicate literal member `2` +PYI062.py:14:32: PYI062 [*] Duplicate literal member `2` | 12 | Literal[1, 2, Literal[1, 2]] # twice 13 | Literal[1, Literal[1], Literal[1]] # twice @@ -98,8 +197,19 @@ PYI062.py:14:32: PYI062 Duplicate literal member `2` 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once 16 | typing_extensions.Literal[1, 1, 1] # twice | + = help: Remove duplicates -PYI062.py:15:37: PYI062 Duplicate literal member `1` +ℹ Safe fix +11 11 | Literal[1, Literal[1]] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 |-Literal[1, Literal[2], Literal[2]] # once + 14 |+Literal[1, 2] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 16 | typing_extensions.Literal[1, 1, 1] # twice +17 17 | + +PYI062.py:15:37: PYI062 [*] Duplicate literal member `1` | 13 | Literal[1, Literal[1], Literal[1]] # twice 14 | Literal[1, Literal[2], Literal[2]] # once @@ -107,8 +217,19 @@ PYI062.py:15:37: PYI062 Duplicate literal member `1` | ^ PYI062 16 | typing_extensions.Literal[1, 1, 1] # twice | + = help: Remove duplicates + +ℹ Safe fix +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 |-t.Literal[1, t.Literal[2, t.Literal[1]]] # once + 15 |+t.Literal[1, 2] # once +16 16 | typing_extensions.Literal[1, 1, 1] # twice +17 17 | +18 18 | # Ensure issue is only raised once, even on nested literals -PYI062.py:16:30: PYI062 Duplicate literal member `1` +PYI062.py:16:30: PYI062 [*] Duplicate literal member `1` | 14 | Literal[1, Literal[2], Literal[2]] # once 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once @@ -117,8 +238,19 @@ PYI062.py:16:30: PYI062 Duplicate literal member `1` 17 | 18 | # Ensure issue is only raised once, even on nested literals | + = help: Remove duplicates -PYI062.py:16:33: PYI062 Duplicate literal member `1` +ℹ Safe fix +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 |-typing_extensions.Literal[1, 1, 1] # twice + 16 |+typing_extensions.Literal[1] # twice +17 17 | +18 18 | # Ensure issue is only raised once, even on nested literals +19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062 + +PYI062.py:16:33: PYI062 [*] Duplicate literal member `1` | 14 | Literal[1, Literal[2], Literal[2]] # once 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once @@ -127,8 +259,19 @@ PYI062.py:16:33: PYI062 Duplicate literal member `1` 17 | 18 | # Ensure issue is only raised once, even on nested literals | + = help: Remove duplicates + +ℹ Safe fix +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 |-typing_extensions.Literal[1, 1, 1] # twice + 16 |+typing_extensions.Literal[1] # twice +17 17 | +18 18 | # Ensure issue is only raised once, even on nested literals +19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062 -PYI062.py:19:46: PYI062 Duplicate literal member `True` +PYI062.py:19:46: PYI062 [*] Duplicate literal member `True` | 18 | # Ensure issue is only raised once, even on nested literals 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062 @@ -136,3 +279,13 @@ PYI062.py:19:46: PYI062 Duplicate literal member `True` 20 | 21 | n: Literal["No", "duplicates", "here", 1, "1"] | + = help: Remove duplicates + +ℹ Safe fix +16 16 | typing_extensions.Literal[1, 1, 1] # twice +17 17 | +18 18 | # Ensure issue is only raised once, even on nested literals +19 |-MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062 + 19 |+MyType = Literal["foo", True, False, "bar"] # PYI062 +20 20 | +21 21 | n: Literal["No", "duplicates", "here", 1, "1"] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap index 069024753f74b..0a96dea0cec1e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs --- -PYI062.pyi:5:25: PYI062 Duplicate literal member `True` +PYI062.pyi:5:25: PYI062 [*] Duplicate literal member `True` | 3 | import typing_extensions 4 | @@ -10,8 +10,19 @@ PYI062.pyi:5:25: PYI062 Duplicate literal member `True` 6 | 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1 | + = help: Remove duplicates -PYI062.pyi:5:31: PYI062 Duplicate literal member `False` +ℹ Safe fix +2 2 | import typing as t +3 3 | import typing_extensions +4 4 | +5 |-x: Literal[True, False, True, False] # PY062 twice here + 5 |+x: Literal[True, False] # PY062 twice here +6 6 | +7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1 +8 8 | + +PYI062.pyi:5:31: PYI062 [*] Duplicate literal member `False` | 3 | import typing_extensions 4 | @@ -20,8 +31,19 @@ PYI062.pyi:5:31: PYI062 Duplicate literal member `False` 6 | 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1 | + = help: Remove duplicates + +ℹ Safe fix +2 2 | import typing as t +3 3 | import typing_extensions +4 4 | +5 |-x: Literal[True, False, True, False] # PY062 twice here + 5 |+x: Literal[True, False] # PY062 twice here +6 6 | +7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1 +8 8 | -PYI062.pyi:7:45: PYI062 Duplicate literal member `1` +PYI062.pyi:7:45: PYI062 [*] Duplicate literal member `1` | 5 | x: Literal[True, False, True, False] # PY062 twice here 6 | @@ -30,8 +52,19 @@ PYI062.pyi:7:45: PYI062 Duplicate literal member `1` 8 | 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal | + = help: Remove duplicates -PYI062.pyi:9:33: PYI062 Duplicate literal member `{1, 3, 5}` +ℹ Safe fix +4 4 | +5 5 | x: Literal[True, False, True, False] # PY062 twice here +6 6 | +7 |-y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1 + 7 |+y: Literal[1, print("hello"), 3, 4] # PY062 on the last 1 +8 8 | +9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal +10 10 | + +PYI062.pyi:9:33: PYI062 [*] Duplicate literal member `{1, 3, 5}` | 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1 8 | @@ -40,8 +73,19 @@ PYI062.pyi:9:33: PYI062 Duplicate literal member `{1, 3, 5}` 10 | 11 | Literal[1, Literal[1]] # once | + = help: Remove duplicates + +ℹ Safe fix +6 6 | +7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1 +8 8 | +9 |-z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal + 9 |+z: Literal[{1, 3, 5}, "foobar"] # PY062 on the set literal +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice -PYI062.pyi:11:20: PYI062 Duplicate literal member `1` +PYI062.pyi:11:20: PYI062 [*] Duplicate literal member `1` | 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal 10 | @@ -50,8 +94,19 @@ PYI062.pyi:11:20: PYI062 Duplicate literal member `1` 12 | Literal[1, 2, Literal[1, 2]] # twice 13 | Literal[1, Literal[1], Literal[1]] # twice | + = help: Remove duplicates -PYI062.pyi:12:23: PYI062 Duplicate literal member `1` +ℹ Safe fix +8 8 | +9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal +10 10 | +11 |-Literal[1, Literal[1]] # once + 11 |+Literal[1] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once + +PYI062.pyi:12:23: PYI062 [*] Duplicate literal member `1` | 11 | Literal[1, Literal[1]] # once 12 | Literal[1, 2, Literal[1, 2]] # twice @@ -59,8 +114,19 @@ PYI062.pyi:12:23: PYI062 Duplicate literal member `1` 13 | Literal[1, Literal[1], Literal[1]] # twice 14 | Literal[1, Literal[2], Literal[2]] # once | + = help: Remove duplicates + +ℹ Safe fix +9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 |-Literal[1, 2, Literal[1, 2]] # twice + 12 |+Literal[1, 2] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once -PYI062.pyi:12:26: PYI062 Duplicate literal member `2` +PYI062.pyi:12:26: PYI062 [*] Duplicate literal member `2` | 11 | Literal[1, Literal[1]] # once 12 | Literal[1, 2, Literal[1, 2]] # twice @@ -68,8 +134,19 @@ PYI062.pyi:12:26: PYI062 Duplicate literal member `2` 13 | Literal[1, Literal[1], Literal[1]] # twice 14 | Literal[1, Literal[2], Literal[2]] # once | + = help: Remove duplicates + +ℹ Safe fix +9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 |-Literal[1, 2, Literal[1, 2]] # twice + 12 |+Literal[1, 2] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once -PYI062.pyi:13:20: PYI062 Duplicate literal member `1` +PYI062.pyi:13:20: PYI062 [*] Duplicate literal member `1` | 11 | Literal[1, Literal[1]] # once 12 | Literal[1, 2, Literal[1, 2]] # twice @@ -78,8 +155,19 @@ PYI062.pyi:13:20: PYI062 Duplicate literal member `1` 14 | Literal[1, Literal[2], Literal[2]] # once 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once | + = help: Remove duplicates -PYI062.pyi:13:32: PYI062 Duplicate literal member `1` +ℹ Safe fix +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 |-Literal[1, Literal[1], Literal[1]] # twice + 13 |+Literal[1] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 16 | typing_extensions.Literal[1, 1, 1] # twice + +PYI062.pyi:13:32: PYI062 [*] Duplicate literal member `1` | 11 | Literal[1, Literal[1]] # once 12 | Literal[1, 2, Literal[1, 2]] # twice @@ -88,8 +176,19 @@ PYI062.pyi:13:32: PYI062 Duplicate literal member `1` 14 | Literal[1, Literal[2], Literal[2]] # once 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once | + = help: Remove duplicates + +ℹ Safe fix +10 10 | +11 11 | Literal[1, Literal[1]] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 |-Literal[1, Literal[1], Literal[1]] # twice + 13 |+Literal[1] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 16 | typing_extensions.Literal[1, 1, 1] # twice -PYI062.pyi:14:32: PYI062 Duplicate literal member `2` +PYI062.pyi:14:32: PYI062 [*] Duplicate literal member `2` | 12 | Literal[1, 2, Literal[1, 2]] # twice 13 | Literal[1, Literal[1], Literal[1]] # twice @@ -98,8 +197,19 @@ PYI062.pyi:14:32: PYI062 Duplicate literal member `2` 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once 16 | typing_extensions.Literal[1, 1, 1] # twice | + = help: Remove duplicates -PYI062.pyi:15:37: PYI062 Duplicate literal member `1` +ℹ Safe fix +11 11 | Literal[1, Literal[1]] # once +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 |-Literal[1, Literal[2], Literal[2]] # once + 14 |+Literal[1, 2] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 16 | typing_extensions.Literal[1, 1, 1] # twice +17 17 | + +PYI062.pyi:15:37: PYI062 [*] Duplicate literal member `1` | 13 | Literal[1, Literal[1], Literal[1]] # twice 14 | Literal[1, Literal[2], Literal[2]] # once @@ -107,8 +217,19 @@ PYI062.pyi:15:37: PYI062 Duplicate literal member `1` | ^ PYI062 16 | typing_extensions.Literal[1, 1, 1] # twice | + = help: Remove duplicates + +ℹ Safe fix +12 12 | Literal[1, 2, Literal[1, 2]] # twice +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 |-t.Literal[1, t.Literal[2, t.Literal[1]]] # once + 15 |+t.Literal[1, 2] # once +16 16 | typing_extensions.Literal[1, 1, 1] # twice +17 17 | +18 18 | # Ensure issue is only raised once, even on nested literals -PYI062.pyi:16:30: PYI062 Duplicate literal member `1` +PYI062.pyi:16:30: PYI062 [*] Duplicate literal member `1` | 14 | Literal[1, Literal[2], Literal[2]] # once 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once @@ -117,8 +238,19 @@ PYI062.pyi:16:30: PYI062 Duplicate literal member `1` 17 | 18 | # Ensure issue is only raised once, even on nested literals | + = help: Remove duplicates -PYI062.pyi:16:33: PYI062 Duplicate literal member `1` +ℹ Safe fix +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 |-typing_extensions.Literal[1, 1, 1] # twice + 16 |+typing_extensions.Literal[1] # twice +17 17 | +18 18 | # Ensure issue is only raised once, even on nested literals +19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062 + +PYI062.pyi:16:33: PYI062 [*] Duplicate literal member `1` | 14 | Literal[1, Literal[2], Literal[2]] # once 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once @@ -127,8 +259,19 @@ PYI062.pyi:16:33: PYI062 Duplicate literal member `1` 17 | 18 | # Ensure issue is only raised once, even on nested literals | + = help: Remove duplicates + +ℹ Safe fix +13 13 | Literal[1, Literal[1], Literal[1]] # twice +14 14 | Literal[1, Literal[2], Literal[2]] # once +15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once +16 |-typing_extensions.Literal[1, 1, 1] # twice + 16 |+typing_extensions.Literal[1] # twice +17 17 | +18 18 | # Ensure issue is only raised once, even on nested literals +19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062 -PYI062.pyi:19:46: PYI062 Duplicate literal member `True` +PYI062.pyi:19:46: PYI062 [*] Duplicate literal member `True` | 18 | # Ensure issue is only raised once, even on nested literals 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062 @@ -136,3 +279,13 @@ PYI062.pyi:19:46: PYI062 Duplicate literal member `True` 20 | 21 | n: Literal["No", "duplicates", "here", 1, "1"] | + = help: Remove duplicates + +ℹ Safe fix +16 16 | typing_extensions.Literal[1, 1, 1] # twice +17 17 | +18 18 | # Ensure issue is only raised once, even on nested literals +19 |-MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062 + 19 |+MyType = Literal["foo", True, False, "bar"] # PYI062 +20 20 | +21 21 | n: Literal["No", "duplicates", "here", 1, "1"] From d1ef418bb01ee6eb2289c5d5b42a23dcd49ad59b Mon Sep 17 00:00:00 2001 From: Simon Brugman Date: Fri, 8 Nov 2024 10:01:53 +0100 Subject: [PATCH 05/16] Docs: tweak rules documentation (#14180) Co-authored-by: Micha Reiser Co-authored-by: Alex Waygood --- .../flake8_annotations/rules/definition.rs | 2 +- .../flake8_bandit/rules/suspicious_imports.rs | 15 ++++-- .../rules/tarfile_unsafe_members.rs | 4 +- .../flake8_blind_except/rules/blind_except.rs | 2 +- .../rules/abstract_base_class.rs | 2 +- .../rules/f_string_docstring.rs | 2 +- .../rules/function_uses_loop_variable.rs | 2 +- .../rules/no_explicit_stacklevel.rs | 3 ++ .../rules/useless_contextlib_suppress.rs | 2 +- ...cessary_dict_comprehension_for_iterable.rs | 5 +- .../rules/f_string_in_gettext_func_call.rs | 2 +- .../rules/format_in_gettext_func_call.rs | 2 +- .../rules/printf_in_gettext_func_call.rs | 2 +- .../rules/implicit_namespace_package.rs | 2 +- .../rules/complex_if_statement_in_stub.rs | 2 +- .../flake8_pyi/rules/non_empty_stub_body.rs | 3 +- .../flake8_pyi/rules/non_self_return_type.rs | 2 +- .../rules/pass_statement_stub_body.rs | 3 +- .../rules/quoted_annotation_in_stub.rs | 2 +- .../rules/redundant_numeric_union.rs | 2 +- .../flake8_pyi/rules/unrecognized_platform.rs | 4 +- .../rules/unrecognized_version_info.rs | 6 +-- .../flake8_pytest_style/rules/fixture.rs | 2 +- .../rules/flake8_pytest_style/rules/patch.rs | 2 +- .../rules/empty_type_checking_block.rs | 2 +- .../runtime_import_in_type_checking_block.rs | 2 +- .../rules/runtime_string_union.rs | 4 +- .../rules/typing_only_runtime_import.rs | 6 +-- .../rules/unused_arguments.rs | 35 ++++++++++++++ .../rules/os_path_getatime.rs | 2 +- .../rules/os_path_getctime.rs | 2 +- .../rules/os_path_getmtime.rs | 2 +- .../rules/os_path_getsize.rs | 2 +- .../rules/flake8_use_pathlib/violations.rs | 48 +++++++++---------- .../pandas_vet/rules/inplace_argument.rs | 2 +- .../rules/camelcase_imported_as_acronym.rs | 4 +- .../rules/pycodestyle/rules/blank_lines.rs | 12 ++--- .../rules/f_string_missing_placeholders.rs | 2 +- .../rules/forward_annotation_syntax_error.rs | 2 +- .../pyflakes/rules/starred_expressions.rs | 5 +- .../rules/pyflakes/rules/unused_annotation.rs | 2 +- .../rules/pylint/rules/await_outside_async.rs | 2 +- .../pylint/rules/bidirectional_unicode.rs | 4 +- .../src/rules/pylint/rules/if_stmt_min_max.rs | 4 +- .../rules/pylint/rules/import_private_name.rs | 4 +- .../src/rules/pylint/rules/import_self.rs | 3 +- .../rules/load_before_global_declaration.rs | 2 +- .../src/rules/pylint/rules/no_self_use.rs | 9 ++++ .../pylint/rules/nonlocal_without_binding.rs | 2 +- .../rules/type_name_incorrect_variance.rs | 2 +- .../pylint/rules/useless_import_alias.rs | 6 +++ .../pyupgrade/rules/quoted_annotation.rs | 2 +- .../rules/unnecessary_default_type_args.rs | 7 ++- .../pyupgrade/rules/useless_metaclass_type.rs | 2 +- .../rules/useless_object_inheritance.rs | 2 +- .../pyupgrade/rules/yield_in_for_loop.rs | 2 +- 56 files changed, 164 insertions(+), 98 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index 6f8590b957552..9f94b29cc53cd 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -469,7 +469,7 @@ impl Violation for MissingReturnTypeClassMethod { /// ``` /// /// ## References -/// - [PEP 484](https://www.python.org/dev/peps/pep-0484/#the-any-type) +/// - [Typing spec: `Any`](https://typing.readthedocs.io/en/latest/spec/special-types.html#any) /// - [Python documentation: `typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any) /// - [Mypy documentation: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type) #[violation] diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs index 3e4a0f6284a1b..fb40986448824 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs @@ -10,16 +10,21 @@ use crate::checkers::ast::Checker; use crate::registry::AsRule; /// ## What it does -/// Checks for imports of the`telnetlib` module. +/// Checks for imports of the `telnetlib` module. /// /// ## Why is this bad? -/// Telnet is considered insecure. Instead, use SSH or another encrypted +/// Telnet is considered insecure. It is deprecated since version 3.11, and +/// was removed in version 3.13. Instead, use SSH or another encrypted /// protocol. /// /// ## Example /// ```python /// import telnetlib /// ``` +/// +/// ## References +/// - [Python documentation: `telnetlib` - Telnet client](https://docs.python.org/3.12/library/telnetlib.html#module-telnetlib) +/// - [PEP 594: `telnetlib`](https://peps.python.org/pep-0594/#telnetlib) #[violation] pub struct SuspiciousTelnetlibImport; @@ -41,6 +46,9 @@ impl Violation for SuspiciousTelnetlibImport { /// ```python /// import ftplib /// ``` +/// +/// ## References +/// - [Python documentation: `ftplib` - FTP protocol client](https://docs.python.org/3/library/ftplib.html) #[violation] pub struct SuspiciousFtplibImport; @@ -63,8 +71,9 @@ impl Violation for SuspiciousFtplibImport { /// ```python /// import pickle /// ``` +/// /// ## References -/// - [Python Docs](https://docs.python.org/3/library/pickle.html) +/// - [Python documentation: `pickle` — Python object serialization](https://docs.python.org/3/library/pickle.html) #[violation] pub struct SuspiciousPickleImport; diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs index 2d3e12fa72f67..fbf30606fed86 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs @@ -33,8 +33,8 @@ use ruff_text_size::Ranged; /// /// ## References /// - [Common Weakness Enumeration: CWE-22](https://cwe.mitre.org/data/definitions/22.html) -/// - [Python Documentation: `TarFile.extractall`](https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall) -/// - [Python Documentation: Extraction filters](https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter) +/// - [Python documentation: `TarFile.extractall`](https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall) +/// - [Python documentation: Extraction filters](https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter) /// /// [PEP 706]: https://peps.python.org/pep-0706/#backporting-forward-compatibility #[violation] diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs index 8682a1268cad8..4d9c1a85f215f 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs @@ -60,7 +60,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) /// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) -/// - [PEP8 Programming Recommendations on bare `except`](https://peps.python.org/pep-0008/#programming-recommendations) +/// - [PEP 8: Programming Recommendations on bare `except`](https://peps.python.org/pep-0008/#programming-recommendations) #[violation] pub struct BlindExcept { name: String, diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs index 410c210b174a5..0d94b43cedfd1 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs @@ -88,7 +88,7 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod { /// ``` /// /// ## References -/// - [Python documentation: abc](https://docs.python.org/3/library/abc.html) +/// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html) #[violation] pub struct EmptyMethodWithoutAbstractDecorator { name: String, diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs index 3294c20419ecc..d89adf5188843 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -28,7 +28,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [PEP 257](https://peps.python.org/pep-0257/) +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) /// - [Python documentation: Formatted string literals](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[violation] pub struct FStringDocstring; diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index 58822804a6054..ae4e7323cbf0a 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -40,7 +40,7 @@ use crate::checkers::ast::Checker; /// /// ## References /// - [The Hitchhiker's Guide to Python: Late Binding Closures](https://docs.python-guide.org/writing/gotchas/#late-binding-closures) -/// - [Python documentation: functools.partial](https://docs.python.org/3/library/functools.html#functools.partial) +/// - [Python documentation: `functools.partial`](https://docs.python.org/3/library/functools.html#functools.partial) #[violation] pub struct FunctionUsesLoopVariable { name: String, diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index b8346044c6ce4..1eb1355a4ea52 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -26,6 +26,9 @@ use crate::checkers::ast::Checker; /// ```python /// warnings.warn("This is a warning", stacklevel=2) /// ``` +/// +/// ## References +/// - [Python documentation: `warnings.warn`](https://docs.python.org/3/library/warnings.html#warnings.warn) #[violation] pub struct NoExplicitStacklevel; diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index 0f5ec106894fd..61e01738e2317 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation: contextlib.suppress](https://docs.python.org/3/library/contextlib.html#contextlib.suppress) +/// - [Python documentation: `contextlib.suppress`](https://docs.python.org/3/library/contextlib.html#contextlib.suppress) #[violation] pub struct UselessContextlibSuppress; diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs index 964140e7e54be..d5fbfd7a4f69c 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs @@ -30,6 +30,9 @@ use crate::checkers::ast::Checker; /// dict.fromkeys(iterable) /// dict.fromkeys(iterable, 1) /// ``` +/// +/// ## References +/// - [Python documentation: `dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys) #[violation] pub struct UnnecessaryDictComprehensionForIterable { is_value_none_literal: bool, @@ -53,7 +56,7 @@ impl Violation for UnnecessaryDictComprehensionForIterable { } } -/// RUF025 +/// C420 pub(crate) fn unnecessary_dict_comprehension_for_iterable( checker: &mut Checker, dict_comp: &ast::ExprDictComp, diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs index 1d731836b1a98..b46e523d11bc7 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs @@ -39,7 +39,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html) +/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html) #[violation] pub struct FStringInGetTextFuncCall; diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs index 136fb936707e0..712c6e6df36e6 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs @@ -39,7 +39,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html) +/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html) #[violation] pub struct FormatInGetTextFuncCall; diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs index 3cc4bcec1ea81..7d68d75f77c4e 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs @@ -38,7 +38,7 @@ use ruff_text_size::Ranged; /// ``` /// /// ## References -/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html) +/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html) #[violation] pub struct PrintfInGetTextFuncCall; diff --git a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs index 76fec32529558..dd8da4a5990e7 100644 --- a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs +++ b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs @@ -22,7 +22,7 @@ use crate::Locator; /// /// Directories that lack an `__init__.py` file can still be imported, but /// they're indicative of a special kind of package, known as a "namespace -/// package" (see: [PEP 420](https://www.python.org/dev/peps/pep-0420/)). +/// package" (see: [PEP 420](https://peps.python.org/pep-0420/)). /// Namespace packages are less widely used, so a package that lacks an /// `__init__.py` file is typically meant to be a regular package, and /// the absence of the `__init__.py` file is probably an oversight. diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs index 0d9e6fbd1550a..ca075c48bc5f6 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs @@ -30,7 +30,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// The [typing documentation on stub files](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks) +/// - [Typing documentation: Version and platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks) #[violation] pub struct ComplexIfStatementInStub; diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs index 9e98c4671f1cb..4d867e08865c6 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs @@ -26,8 +26,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [The recommended style for stub functions and methods](https://typing.readthedocs.io/en/latest/source/stubs.html#id6) -/// in the typing docs. +/// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.readthedocs.io/en/latest/guides/writing_stubs.html) #[violation] pub struct NonEmptyStubBody; diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index ea6c1f0877f6a..709bea3204494 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -71,7 +71,7 @@ use crate::checkers::ast::Checker; /// def __iadd__(self, other: Foo) -> Self: ... /// ``` /// ## References -/// - [`typing.Self` documentation](https://docs.python.org/3/library/typing.html#typing.Self) +/// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) #[violation] pub struct NonSelfReturnType { class_name: String, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs index 17381eeb0a1d1..b6c67ff3fc7ed 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs @@ -23,8 +23,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// The [recommended style for functions and methods](https://typing.readthedocs.io/en/latest/source/stubs.html#functions-and-methods) -/// in the typing docs. +/// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.readthedocs.io/en/latest/guides/writing_stubs.html) #[violation] pub struct PassStatementStubBody; diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs index f54ed6fcffaf7..d58e82034b661 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs @@ -27,7 +27,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Static Typing with Python: Type Stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) +/// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.readthedocs.io/en/latest/guides/writing_stubs.html) #[violation] pub struct QuotedAnnotationInStub; diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs index 00c4296fa658b..39251a474170b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [The typing specification](https://docs.python.org/3/library/numbers.html#the-numeric-tower) +/// - [Python documentation: The numeric tower](https://docs.python.org/3/library/numbers.html#the-numeric-tower) /// - [PEP 484: The numeric tower](https://peps.python.org/pep-0484/#the-numeric-tower) /// /// [typing specification]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs index db69fcab8d689..ba1e1726c585c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -40,7 +40,7 @@ use crate::registry::Rule; /// ``` /// /// ## References -/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks) +/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks) #[violation] pub struct UnrecognizedPlatformCheck; @@ -74,7 +74,7 @@ impl Violation for UnrecognizedPlatformCheck { /// ``` /// /// ## References -/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks) +/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks) #[violation] pub struct UnrecognizedPlatformName { platform: String, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs index 21fbfe054d1b6..ac282eebf1fb5 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs @@ -31,7 +31,7 @@ use crate::registry::Rule; /// ``` /// /// ## References -/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks) +/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks) #[violation] pub struct UnrecognizedVersionInfoCheck; @@ -70,7 +70,7 @@ impl Violation for UnrecognizedVersionInfoCheck { /// ``` /// /// ## References -/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks) +/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks) #[violation] pub struct PatchVersionComparison; @@ -106,7 +106,7 @@ impl Violation for PatchVersionComparison { /// ``` /// /// ## References -/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks) +/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks) #[violation] pub struct WrongTupleLengthVersionComparison { expected_length: usize, diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs index 004f57cfcc941..842d389f3d1b3 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs @@ -590,7 +590,7 @@ impl AlwaysFixableViolation for PytestErroneousUseFixturesOnFixture { /// ``` /// /// ## References -/// - [`pytest-asyncio`](https://pypi.org/project/pytest-asyncio/) +/// - [PyPI: `pytest-asyncio`](https://pypi.org/project/pytest-asyncio/) #[violation] pub struct PytestUnnecessaryAsyncioMarkOnFixture; diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs index 6f9d22ef75c77..0c5d680c8b8cb 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs @@ -38,7 +38,7 @@ use ruff_text_size::Ranged; /// /// ## References /// - [Python documentation: `unittest.mock.patch`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) -/// - [`pytest-mock`](https://pypi.org/project/pytest-mock/) +/// - [PyPI: `pytest-mock`](https://pypi.org/project/pytest-mock/) #[violation] pub struct PytestPatchWithLambda; diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs index 7ec72358d23c7..2eb141ca3b08f 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs @@ -31,7 +31,7 @@ use crate::fix; /// ``` /// /// ## References -/// - [PEP 535](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) +/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct EmptyTypeCheckingBlock; diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index fda7243224fd5..cbb55d82fef10 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -51,7 +51,7 @@ use crate::rules::flake8_type_checking::imports::ImportBinding; /// - `lint.flake8-type-checking.quote-annotations` /// /// ## References -/// - [PEP 535](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) +/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct RuntimeImportInTypeCheckingBlock { qualified_name: String, diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs index 5e8772026c56b..380115e38deb8 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs @@ -37,8 +37,8 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [PEP 535](https://peps.python.org/pep-0563/) -/// - [PEP 604](https://peps.python.org/pep-0604/) +/// - [PEP 563 - Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/) +/// - [PEP 604 – Allow writing union types as `X | Y`](https://peps.python.org/pep-0604/) /// /// [PEP 604]: https://peps.python.org/pep-0604/ #[violation] diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index aad33b1165a34..a9a43b4ec86b7 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -71,7 +71,7 @@ use crate::rules::isort::{categorize, ImportSection, ImportType}; /// - `lint.typing-modules` /// /// ## References -/// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) +/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyFirstPartyImport { qualified_name: String, @@ -146,7 +146,7 @@ impl Violation for TypingOnlyFirstPartyImport { /// - `lint.typing-modules` /// /// ## References -/// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) +/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyThirdPartyImport { qualified_name: String, @@ -221,7 +221,7 @@ impl Violation for TypingOnlyThirdPartyImport { /// - `lint.typing-modules` /// /// ## References -/// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) +/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyStandardLibraryImport { qualified_name: String, diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index ab753df218539..1405823ba8a8d 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -19,6 +19,10 @@ use crate::registry::Rule; /// An argument that is defined but not used is likely a mistake, and should /// be removed to avoid confusion. /// +/// If a variable is intentionally defined-but-not-used, it should be +/// prefixed with an underscore, or some other value that adheres to the +/// [`lint.dummy-variable-rgx`] pattern. +/// /// ## Example /// ```python /// def foo(bar, baz): @@ -30,6 +34,9 @@ use crate::registry::Rule; /// def foo(bar): /// return bar * 2 /// ``` +/// +/// ## Options +/// - `lint.dummy-variable-rgx` #[violation] pub struct UnusedFunctionArgument { name: String, @@ -50,6 +57,10 @@ impl Violation for UnusedFunctionArgument { /// An argument that is defined but not used is likely a mistake, and should /// be removed to avoid confusion. /// +/// If a variable is intentionally defined-but-not-used, it should be +/// prefixed with an underscore, or some other value that adheres to the +/// [`lint.dummy-variable-rgx`] pattern. +/// /// ## Example /// ```python /// class Class: @@ -63,6 +74,9 @@ impl Violation for UnusedFunctionArgument { /// def foo(self, arg1): /// print(arg1) /// ``` +/// +/// ## Options +/// - `lint.dummy-variable-rgx` #[violation] pub struct UnusedMethodArgument { name: String, @@ -83,6 +97,10 @@ impl Violation for UnusedMethodArgument { /// An argument that is defined but not used is likely a mistake, and should /// be removed to avoid confusion. /// +/// If a variable is intentionally defined-but-not-used, it should be +/// prefixed with an underscore, or some other value that adheres to the +/// [`lint.dummy-variable-rgx`] pattern. +/// /// ## Example /// ```python /// class Class: @@ -98,6 +116,9 @@ impl Violation for UnusedMethodArgument { /// def foo(cls, arg1): /// print(arg1) /// ``` +/// +/// ## Options +/// - `lint.dummy-variable-rgx` #[violation] pub struct UnusedClassMethodArgument { name: String, @@ -118,6 +139,10 @@ impl Violation for UnusedClassMethodArgument { /// An argument that is defined but not used is likely a mistake, and should /// be removed to avoid confusion. /// +/// If a variable is intentionally defined-but-not-used, it should be +/// prefixed with an underscore, or some other value that adheres to the +/// [`lint.dummy-variable-rgx`] pattern. +/// /// ## Example /// ```python /// class Class: @@ -133,6 +158,9 @@ impl Violation for UnusedClassMethodArgument { /// def foo(arg1): /// print(arg1) /// ``` +/// +/// ## Options +/// - `lint.dummy-variable-rgx` #[violation] pub struct UnusedStaticMethodArgument { name: String, @@ -154,6 +182,10 @@ impl Violation for UnusedStaticMethodArgument { /// An argument that is defined but not used is likely a mistake, and should /// be removed to avoid confusion. /// +/// If a variable is intentionally defined-but-not-used, it should be +/// prefixed with an underscore, or some other value that adheres to the +/// [`lint.dummy-variable-rgx`] pattern. +/// /// ## Example /// ```python /// my_list = [1, 2, 3, 4, 5] @@ -165,6 +197,9 @@ impl Violation for UnusedStaticMethodArgument { /// my_list = [1, 2, 3, 4, 5] /// squares = map(lambda x: x**2, my_list) /// ``` +/// +/// ## Options +/// - `lint.dummy-variable-rgx` #[violation] pub struct UnusedLambdaArgument { name: String, diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs index 839d61fae0662..e7f916ebe1c69 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs @@ -32,7 +32,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ## References /// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) /// - [Python documentation: `os.path.getatime`](https://docs.python.org/3/library/os.path.html#os.path.getatime) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs index 5b73c2a9d1a70..8fddb4570f2a7 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs @@ -32,7 +32,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ## References /// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) /// - [Python documentation: `os.path.getctime`](https://docs.python.org/3/library/os.path.html#os.path.getctime) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs index 9163b0f287310..d9b0a4b08e47b 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs @@ -32,7 +32,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ## References /// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) /// - [Python documentation: `os.path.getmtime`](https://docs.python.org/3/library/os.path.html#os.path.getmtime) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs index 02ee01d2d019c..36307ebf0a615 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs @@ -32,7 +32,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ## References /// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) /// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs index 2123711401985..b72155306ec8a 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs @@ -30,7 +30,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ## References /// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve) /// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -73,7 +73,7 @@ impl Violation for OsPathAbspath { /// ## References /// - [Python documentation: `Path.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod) /// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -116,7 +116,7 @@ impl Violation for OsChmod { /// ## References /// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir) /// - [Python documentation: `os.makedirs`](https://docs.python.org/3/library/os.html#os.makedirs) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -159,7 +159,7 @@ impl Violation for OsMakedirs { /// ## References /// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir) /// - [Python documentation: `os.mkdir`](https://docs.python.org/3/library/os.html#os.mkdir) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -202,7 +202,7 @@ impl Violation for OsMkdir { /// ## References /// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename) /// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -245,7 +245,7 @@ impl Violation for OsRename { /// ## References /// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace) /// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -288,7 +288,7 @@ impl Violation for OsReplace { /// ## References /// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir) /// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -331,7 +331,7 @@ impl Violation for OsRmdir { /// ## References /// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink) /// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -374,7 +374,7 @@ impl Violation for OsRemove { /// ## References /// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink) /// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -418,7 +418,7 @@ impl Violation for OsUnlink { /// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd) /// - [Python documentation: `os.getcwd`](https://docs.python.org/3/library/os.html#os.getcwd) /// - [Python documentation: `os.getcwdb`](https://docs.python.org/3/library/os.html#os.getcwdb) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -461,7 +461,7 @@ impl Violation for OsGetcwd { /// ## References /// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists) /// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -504,7 +504,7 @@ impl Violation for OsPathExists { /// ## References /// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser) /// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -547,7 +547,7 @@ impl Violation for OsPathExpanduser { /// ## References /// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir) /// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -590,7 +590,7 @@ impl Violation for OsPathIsdir { /// ## References /// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file) /// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -633,7 +633,7 @@ impl Violation for OsPathIsfile { /// ## References /// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink) /// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -676,7 +676,7 @@ impl Violation for OsPathIslink { /// ## References /// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline) /// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -728,7 +728,7 @@ impl Violation for OsReadlink { /// - [Python documentation: `Path.group`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.group) /// - [Python documentation: `Path.owner`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.owner) /// - [Python documentation: `os.stat`](https://docs.python.org/3/library/os.html#os.stat) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -774,7 +774,7 @@ impl Violation for OsStat { /// ## References /// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute) /// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -817,7 +817,7 @@ impl Violation for OsPathIsabs { /// ## References /// - [Python documentation: `PurePath.joinpath`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.joinpath) /// - [Python documentation: `os.path.join`](https://docs.python.org/3/library/os.path.html#os.path.join) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -877,7 +877,7 @@ pub(crate) enum Joiner { /// ## References /// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name) /// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -920,7 +920,7 @@ impl Violation for OsPathBasename { /// ## References /// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent) /// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -963,7 +963,7 @@ impl Violation for OsPathDirname { /// ## References /// - [Python documentation: `Path.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile) /// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -1015,7 +1015,7 @@ impl Violation for OsPathSamefile { /// - [Python documentation: `Path.suffix`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix) /// - [Python documentation: `Path.suffixes`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffixes) /// - [Python documentation: `os.path.splitext`](https://docs.python.org/3/library/os.path.html#os.path.splitext) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) @@ -1055,7 +1055,7 @@ impl Violation for OsPathSplitext { /// ## References /// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open) /// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) -/// - [PEP 428](https://peps.python.org/pep-0428/) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) /// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs index ef15831d6b82b..66c6877bcf72e 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs @@ -33,7 +33,7 @@ use crate::Locator; /// ``` /// /// ## References -/// - [_Why You Should Probably Never Use pandas inplace=True_](https://towardsdatascience.com/why-you-should-probably-never-use-pandas-inplace-true-9f9f211849e4) +/// - [_Why You Should Probably Never Use pandas `inplace=True`_](https://towardsdatascience.com/why-you-should-probably-never-use-pandas-inplace-true-9f9f211849e4) #[violation] pub struct PandasUseOfInplaceArgument; diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs index 0ce8db355371b..bb26d2190b607 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs @@ -35,10 +35,10 @@ use crate::rules::pep8_naming::helpers; /// from example import MyClassName /// ``` /// -/// [PEP 8]: https://peps.python.org/pep-0008/ -/// /// ## Options /// - `lint.flake8-import-conventions.aliases` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/ #[violation] pub struct CamelcaseImportedAsAcronym { name: String, diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs index 88a6f2c3e59a0..59867fa15cd95 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs @@ -61,7 +61,7 @@ const BLANK_LINES_NESTED_LEVEL: u32 = 1; /// them. That's why this rule is not enabled in typing stub files. /// /// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html) /// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] @@ -114,7 +114,7 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods { /// - `lint.isort.lines-after-imports` /// /// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html) /// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] @@ -181,7 +181,7 @@ impl AlwaysFixableViolation for BlankLinesTopLevel { /// - `lint.isort.lines-between-types` /// /// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html) /// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] @@ -228,7 +228,7 @@ impl AlwaysFixableViolation for TooManyBlankLines { /// ``` /// /// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E304.html) #[violation] pub struct BlankLineAfterDecorator { @@ -278,7 +278,7 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator { /// them. That's why this rule is not enabled in typing stub files. /// /// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html) /// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] @@ -332,7 +332,7 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass { /// them. That's why this rule is not enabled in typing stub files. /// /// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html) /// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index 8c7e99b6e5ef5..97dbc50b45f81 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -52,7 +52,7 @@ use crate::Locator; /// See [#10885](https://github.com/astral-sh/ruff/issues/10885) for more. /// /// ## References -/// - [PEP 498](https://www.python.org/dev/peps/pep-0498/) +/// - [PEP 498 – Literal String Interpolation](https://peps.python.org/pep-0498/) #[violation] pub struct FStringMissingPlaceholders; diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs b/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs index 7e0405111f50c..9c74faf79ff00 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs @@ -21,7 +21,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [PEP 563](https://www.python.org/dev/peps/pep-0563/) +/// - [PEP 563 – Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/) #[violation] pub struct ForwardAnnotationSyntaxError { pub body: String, diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs b/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs index e5b02e6f53a3a..8cdbe6e13a618 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs @@ -13,6 +13,9 @@ use ruff_macros::{derive_message_formats, violation}; /// In Python 3, no more than 1 << 8 assignments are allowed before a starred /// expression, and no more than 1 << 24 expressions are allowed after a starred /// expression. +/// +/// ## References +/// - [PEP 3132 – Extended Iterable Unpacking](https://peps.python.org/pep-3132/) #[violation] pub struct ExpressionsInStarAssignment; @@ -38,7 +41,7 @@ impl Violation for ExpressionsInStarAssignment { /// ``` /// /// ## References -/// - [PEP 3132](https://peps.python.org/pep-3132/) +/// - [PEP 3132 – Extended Iterable Unpacking](https://peps.python.org/pep-3132/) #[violation] pub struct MultipleStarredExpressions; diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs index ad5e383db692f..1310f2dca2f1e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs @@ -19,7 +19,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [PEP 484](https://peps.python.org/pep-0484/) +/// - [PEP 484 – Type Hints](https://peps.python.org/pep-0484/) #[violation] pub struct UnusedAnnotation { name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs index 5ef938af40344..61af369ea4e08 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs @@ -32,7 +32,7 @@ use crate::checkers::ast::Checker; /// /// ## References /// - [Python documentation: Await expression](https://docs.python.org/3/reference/expressions.html#await) -/// - [PEP 492](https://peps.python.org/pep-0492/#await-expression) +/// - [PEP 492: Await Expression](https://peps.python.org/pep-0492/#await-expression) #[violation] pub struct AwaitOutsideAsync; diff --git a/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs b/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs index 5ed2fe368d921..a41cd3b55115b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs @@ -13,7 +13,7 @@ const BIDI_UNICODE: [char; 10] = [ '\u{2068}', //{FIRST STRONG ISOLATE} '\u{2069}', //{POP DIRECTIONAL ISOLATE} // The following was part of PEP 672: - // https://www.python.org/dev/peps/pep-0672/ + // https://peps.python.org/pep-0672/ // so the list above might not be complete '\u{200F}', //{RIGHT-TO-LEFT MARK} // We don't use @@ -40,7 +40,7 @@ const BIDI_UNICODE: [char; 10] = [ /// ``` /// /// ## References -/// - [PEP 672](https://peps.python.org/pep-0672/#bidirectional-text) +/// - [PEP 672: Bidirectional Text](https://peps.python.org/pep-0672/#bidirectional-text) #[violation] pub struct BidirectionalUnicode; diff --git a/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs b/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs index f83280dc28912..1ac7204bd9c55 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs @@ -30,8 +30,8 @@ use crate::fix::snippet::SourceCodeSnippet; /// ``` /// /// ## References -/// - [Python documentation: max function](https://docs.python.org/3/library/functions.html#max) -/// - [Python documentation: min function](https://docs.python.org/3/library/functions.html#min) +/// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) +/// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) #[violation] pub struct IfStmtMinMax { min_max: MinMax, diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs index 1081530d0a9ff..275f5f218382c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs @@ -46,8 +46,8 @@ use crate::checkers::ast::Checker; /// - [PEP 8: Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions) /// - [Semantic Versioning](https://semver.org/) /// -/// [PEP 8]: https://www.python.org/dev/peps/pep-0008/ -/// [PEP 420]: https://www.python.org/dev/peps/pep-0420/ +/// [PEP 8]: https://peps.python.org/pep-0008/ +/// [PEP 420]: https://peps.python.org/pep-0420/ #[violation] pub struct ImportPrivateName { name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_self.rs b/crates/ruff_linter/src/rules/pylint/rules/import_self.rs index 072285eabf690..9ab1185f62754 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_self.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_self.rs @@ -9,7 +9,8 @@ use ruff_text_size::Ranged; /// Checks for import statements that import the current module. /// /// ## Why is this bad? -/// Importing a module from itself is a circular dependency. +/// Importing a module from itself is a circular dependency and results +/// in an `ImportError` exception. /// /// ## Example /// diff --git a/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs index 3545bf6a8674c..5646eead41905 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs @@ -14,7 +14,7 @@ use crate::checkers::ast::Checker; /// ## Why is this bad? /// The `global` declaration applies to the entire scope. Using a name that's /// declared as `global` in a given scope prior to the relevant `global` -/// declaration is a syntax error. +/// declaration is a `SyntaxError`. /// /// ## Example /// ```python diff --git a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs index 62c11b83c0310..f2a032731fab7 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs @@ -28,6 +28,15 @@ use crate::checkers::ast::Checker; /// def greeting(): /// print("Greetings friend!") /// ``` +/// +/// or +/// +/// ```python +/// class Person: +/// @staticmethod +/// def greeting(): +/// print("Greetings friend!") +/// ``` #[violation] pub struct NoSelfUse { method_name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs index d29ef9bf441bd..9ec8a014f4a9d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs @@ -27,7 +27,7 @@ use ruff_macros::{derive_message_formats, violation}; /// /// ## References /// - [Python documentation: The `nonlocal` statement](https://docs.python.org/3/reference/simple_stmts.html#nonlocal) -/// - [PEP 3104](https://peps.python.org/pep-3104/) +/// - [PEP 3104 – Access to Names in Outer Scopes](https://peps.python.org/pep-3104/) #[violation] pub struct NonlocalWithoutBinding { pub(crate) name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs index 75a86035d398d..2c17c1a4623f3 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -41,7 +41,7 @@ use crate::rules::pylint::helpers::type_param_name; /// - [PEP 483 – The Theory of Type Hints: Covariance and Contravariance](https://peps.python.org/pep-0483/#covariance-and-contravariance) /// - [PEP 484 – Type Hints: Covariance and contravariance](https://peps.python.org/pep-0484/#covariance-and-contravariance) /// -/// [PEP 484]: https://www.python.org/dev/peps/pep-0484/ +/// [PEP 484]: https://peps.python.org/pep-0484/ #[violation] pub struct TypeNameIncorrectVariance { kind: VarKind, diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs index 39ed19f7dd4de..8b50c72922ae0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs @@ -21,6 +21,12 @@ use crate::checkers::ast::Checker; /// ```python /// import numpy as np /// ``` +/// +/// or +/// +/// ```python +/// import numpy +/// ``` #[violation] pub struct UselessImportAlias; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs index 9f169656be41c..eccac9bf7ee0b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -55,7 +55,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [PEP 563](https://peps.python.org/pep-0563/) +/// - [PEP 563 – Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/) /// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__) #[violation] pub struct QuotedAnnotation; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs index 3811ee8e8e106..1e26ada05bbfe 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs @@ -45,11 +45,10 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// /// - [PEP 696 – Type Defaults for Type Parameters](https://peps.python.org/pep-0696/) -/// - [Annotating generators and coroutines](https://docs.python.org/3.13/library/typing.html#annotating-generators-and-coroutines) -/// - [typing.Generator](https://docs.python.org/3.13/library/typing.html#typing.Generator) -/// - [typing.AsyncGenerator](https://docs.python.org/3.13/library/typing.html#typing.AsyncGenerator) +/// - [Annotating generators and coroutines](https://docs.python.org/3/library/typing.html#annotating-generators-and-coroutines) +/// - [Python documentation: `typing.Generator`](https://docs.python.org/3/library/typing.html#typing.Generator) +/// - [Python documentation: `typing.AsyncGenerator`](https://docs.python.org/3/library/typing.html#typing.AsyncGenerator) #[violation] pub struct UnnecessaryDefaultTypeArgs; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs index db5792fcf4e77..f3df7fe4fd14f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -27,7 +27,7 @@ use crate::fix; /// ``` /// /// ## References -/// - [PEP 3115](https://www.python.org/dev/peps/pep-3115/) +/// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/) #[violation] pub struct UselessMetaclassType; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs index 2b02baeaa2aa3..bc513b7fb19b4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -26,7 +26,7 @@ use crate::fix::edits::{remove_argument, Parentheses}; /// ``` /// /// ## References -/// - [PEP 3115](https://www.python.org/dev/peps/pep-3115/) +/// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/) #[violation] pub struct UselessObjectInheritance { name: String, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs index f9f0ea4488934..59b2f93b7228d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -36,7 +36,7 @@ use crate::checkers::ast::Checker; /// /// ## References /// - [Python documentation: The `yield` statement](https://docs.python.org/3/reference/simple_stmts.html#the-yield-statement) -/// - [PEP 380](https://peps.python.org/pep-0380/) +/// - [PEP 380 – Syntax for Delegating to a Subgenerator](https://peps.python.org/pep-0380/) #[violation] pub struct YieldInForLoop; From fed35a25e86f8eb86fdeefe43294847ffb4dec34 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 8 Nov 2024 10:53:48 +0100 Subject: [PATCH 06/16] [red-knot] Fix `is_assignable_to` for unions (#14196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix `Type::is_assignable_to` for union types on the left hand side (of `.is_assignable_to`; or the right hand side of the `… = …` assignment): `Literal[1, 2]` should be assignable to `int`. ## Test Plan New unit tests that were previously failing. --- crates/red_knot_python_semantic/src/types.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index cae012ed95dc9..f06ed8ab3677e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -626,6 +626,10 @@ impl<'db> Type<'db> { match (self, target) { (Type::Unknown | Type::Any | Type::Todo, _) => true, (_, Type::Unknown | Type::Any | Type::Todo) => true, + (Type::Union(union), ty) => union + .elements(db) + .iter() + .all(|&elem_ty| elem_ty.is_assignable_to(db, ty)), (ty, Type::Union(union)) => union .elements(db) .iter() @@ -2765,6 +2769,14 @@ mod tests { #[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))] #[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::Unknown, Ty::BuiltinInstance("str")]))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))] + #[test_case( + Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), + Ty::BuiltinInstance("int") + )] + #[test_case( + Ty::Union(vec![Ty::IntLiteral(1), Ty::None]), + Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::None]) + )] #[test_case(Ty::Tuple(vec![Ty::Todo]), Ty::Tuple(vec![Ty::IntLiteral(2)]))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::Todo]))] fn is_assignable_to(from: Ty, to: Ty) { @@ -2776,6 +2788,14 @@ mod tests { #[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("str"))] #[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))] #[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))] + #[test_case( + Ty::Union(vec![Ty::IntLiteral(1), Ty::None]), + Ty::BuiltinInstance("int") + )] + #[test_case( + Ty::Union(vec![Ty::IntLiteral(1), Ty::None]), + Ty::Union(vec![Ty::BuiltinInstance("str"), Ty::None]) + )] fn is_not_assignable_to(from: Ty, to: Ty) { let db = setup_db(); assert!(!from.into_type(&db).is_assignable_to(&db, to.into_type(&db))); From 670f9585258e81539326f0c4a524f1edc6b1df4f Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 8 Nov 2024 10:54:13 +0100 Subject: [PATCH 07/16] [red-knot] Fix intersection simplification for `~Any`/`~Unknown` (#14195) ## Summary Another bug found using [property testing](https://github.com/astral-sh/ruff/pull/14178). ## Test Plan New unit test --- .../src/types/builder.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 295b73bc7ece6..d061971dd37f4 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -317,7 +317,7 @@ impl<'db> InnerIntersectionBuilder<'db> { // Adding any of these types to the negative side of an intersection // is equivalent to adding it to the positive side. We do this to // simplify the representation. - self.positive.insert(ty); + self.add_positive(db, ty); } // ~Literal[True] & bool = Literal[False] Type::BooleanLiteral(bool) @@ -592,6 +592,22 @@ mod tests { assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]"); } + #[test] + fn build_intersection_simplify_negative_any() { + let db = setup_db(); + + let ty = IntersectionBuilder::new(&db) + .add_negative(Type::Any) + .build(); + assert_eq!(ty, Type::Any); + + let ty = IntersectionBuilder::new(&db) + .add_positive(Type::Never) + .add_negative(Type::Any) + .build(); + assert_eq!(ty, Type::Never); + } + #[test] fn intersection_distributes_over_union() { let db = setup_db(); From fbf140a665629ce31191e56918bec6a724a24617 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 8 Nov 2024 16:39:37 +0530 Subject: [PATCH 08/16] Bump version to 0.7.3 (#14197) --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++ Cargo.lock | 6 ++--- README.md | 6 ++--- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/integrations.md | 10 ++++----- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 9 files changed, 53 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de96e929c7a32..763a144e542dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## 0.7.3 + +### Preview features + +- Formatter: Disallow single-line implicit concatenated strings ([#13928](https://github.com/astral-sh/ruff/pull/13928)) +- \[`flake8-pyi`\] Include all Python file types for `PYI006` and `PYI066` ([#14059](https://github.com/astral-sh/ruff/pull/14059)) +- \[`flake8-simplify`\] Implement `split-of-static-string` (`SIM905`) ([#14008](https://github.com/astral-sh/ruff/pull/14008)) +- \[`refurb`\] Implement `subclass-builtin` (`FURB189`) ([#14105](https://github.com/astral-sh/ruff/pull/14105)) +- \[`ruff`\] Improve diagnostic messages and docs (`RUF031`, `RUF032`, `RUF034`) ([#14068](https://github.com/astral-sh/ruff/pull/14068)) + +### Rule changes + +- Detect items that hash to same value in duplicate sets (`B033`, `PLC0208`) ([#14064](https://github.com/astral-sh/ruff/pull/14064)) +- \[`eradicate`\] Better detection of IntelliJ language injection comments (`ERA001`) ([#14094](https://github.com/astral-sh/ruff/pull/14094)) +- \[`flake8-pyi`\] Add autofix for `docstring-in-stub` (`PYI021`) ([#14150](https://github.com/astral-sh/ruff/pull/14150)) +- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to alawys provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188)) +- \[`pyflakes`\] Detect items that hash to same value in duplicate dictionaries (`F601`) ([#14065](https://github.com/astral-sh/ruff/pull/14065)) +- \[`ruff`\] Fix false positive for decorators (`RUF028`) ([#14061](https://github.com/astral-sh/ruff/pull/14061)) + +### Bug fixes + +- Avoid parsing joint rule codes as distinct codes in `# noqa` ([#12809](https://github.com/astral-sh/ruff/pull/12809)) +- \[`eradicate`\] ignore `# language=` in commented-out-code rule (ERA001) ([#14069](https://github.com/astral-sh/ruff/pull/14069)) +- \[`flake8-bugbear`\] - do not run `mutable-argument-default` on stubs (`B006`) ([#14058](https://github.com/astral-sh/ruff/pull/14058)) +- \[`flake8-builtins`\] Skip lambda expressions in `builtin-argument-shadowing (A002)` ([#14144](https://github.com/astral-sh/ruff/pull/14144)) +- \[`flake8-comprehension`\] Also remove trailing comma while fixing `C409` and `C419` ([#14097](https://github.com/astral-sh/ruff/pull/14097)) +- \[`flake8-simplify`\] Allow `open` without context manager in `return` statement (`SIM115`) ([#14066](https://github.com/astral-sh/ruff/pull/14066)) +- \[`pylint`\] Respect hash-equivalent literals in `iteration-over-set` (`PLC0208`) ([#14063](https://github.com/astral-sh/ruff/pull/14063)) +- \[`pylint`\] Update known dunder methods for Python 3.13 (`PLW3201`) ([#14146](https://github.com/astral-sh/ruff/pull/14146)) +- \[`pyupgrade`\] - ignore kwarg unpacking for `UP044` ([#14053](https://github.com/astral-sh/ruff/pull/14053)) +- \[`refurb`\] Parse more exotic decimal strings in `verbose-decimal-constructor` (`FURB157`) ([#14098](https://github.com/astral-sh/ruff/pull/14098)) + +### Documentation + +- Add links to missing related options within rule documentations ([#13971](https://github.com/astral-sh/ruff/pull/13971)) +- Add rule short code to mkdocs tags to allow searching via rule codes ([#14040](https://github.com/astral-sh/ruff/pull/14040)) + ## 0.7.2 ### Preview features diff --git a/Cargo.lock b/Cargo.lock index 1a09eb3febecc..c2078ab16a8d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2317,7 +2317,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.7.2" +version = "0.7.3" dependencies = [ "anyhow", "argfile", @@ -2534,7 +2534,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.7.2" +version = "0.7.3" dependencies = [ "aho-corasick", "annotate-snippets 0.9.2", @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.7.2" +version = "0.7.3" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index 421d56498cfaf..3d4a28a70265f 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.7.2/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.7.2/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.7.3/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.7.3/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -170,7 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.7.2 + rev: v0.7.3 hooks: # Run the linter. - id: ruff diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 492e5fa49eb94..e79f13c691b69 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.7.2" +version = "0.7.3" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index ef2dfd885f6a6..c821d013d5017 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.7.2" +version = "0.7.3" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index af4790bc77beb..510bef7e6c4e6 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.7.2" +version = "0.7.3" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/integrations.md b/docs/integrations.md index 67a735e3a1ae4..8f4f1a0177775 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -80,13 +80,13 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.7.2-alpine + name: ghcr.io/astral-sh/ruff:0.7.3-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version Ruff Check: - extends: .base_ruff + extends: .base_ruff script: - ruff check --output-format=gitlab > code-quality-report.json artifacts: @@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.7.2 + rev: v0.7.3 hooks: # Run the linter. - id: ruff @@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.7.2 + rev: v0.7.3 hooks: # Run the linter. - id: ruff @@ -133,7 +133,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.7.2 + rev: v0.7.3 hooks: # Run the linter. - id: ruff diff --git a/pyproject.toml b/pyproject.toml index f11e8bbe4694f..2a0c4b3685382 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.7.2" +version = "0.7.3" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 985aaba791422..5bc566b137ca5 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "scripts" -version = "0.7.2" +version = "0.7.3" description = "" authors = ["Charles Marsh "] From 953e862aca1449651d96036fcd35f7fab4ccda51 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 8 Nov 2024 11:58:57 +0000 Subject: [PATCH 09/16] [red-knot] Improve error message for metaclass conflict (#14174) --- .../resources/mdtest/metaclass.md | 6 +- crates/red_knot_python_semantic/src/types.rs | 58 ++++++++++++++---- .../src/types/infer.rs | 60 ++++++++++++++----- 3 files changed, 94 insertions(+), 30 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md index 0630b266b37fc..1e9d7266223c7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md @@ -65,7 +65,7 @@ class M2(type): ... class A(metaclass=M1): ... class B(metaclass=M2): ... -# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` and `M2` have no subclass relationship" +# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship" class C(A, B): ... reveal_type(C.__class__) # revealed: Unknown @@ -82,7 +82,7 @@ class M1(type): ... class M2(type): ... class A(metaclass=M1): ... -# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` and `M1` have no subclass relationship" +# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship" class B(A, metaclass=M2): ... reveal_type(B.__class__) # revealed: Unknown @@ -126,7 +126,7 @@ class A(metaclass=M1): ... class B(metaclass=M2): ... class C(metaclass=M12): ... -# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` and `M2` have no subclass relationship" +# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship" class D(A, B, C): ... reveal_type(D.__class__) # revealed: Unknown diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f06ed8ab3677e..a20a05c537d16 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2345,15 +2345,22 @@ impl<'db> Class<'db> { .filter_map(Type::into_class_literal); // Identify the class's own metaclass (or take the first base class's metaclass). - let metaclass = if let Some(metaclass) = class.explicit_metaclass(db) { - metaclass + let explicit_metaclass = class.explicit_metaclass(db); + let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass + { + (metaclass, class) } else if let Some(base_class) = base_classes.next() { - safe_recurse(base_class.class)? + (safe_recurse(base_class.class)?, base_class.class) } else { - KnownClass::Type.to_class(db) + (KnownClass::Type.to_class(db), class) }; - let Type::ClassLiteral(mut candidate) = metaclass else { + let mut candidate = if let Type::ClassLiteral(metaclass_ty) = metaclass { + MetaclassCandidate { + metaclass: metaclass_ty.class, + explicit_metaclass_of: class_metaclass_was_from, + } + } else { // TODO: If the metaclass is not a class, we should verify that it's a callable // which accepts the same arguments as `type.__new__` (otherwise error), and return // the meta-type of its return type. (And validate that is a class type?) @@ -2370,22 +2377,31 @@ impl<'db> Class<'db> { let Type::ClassLiteral(metaclass) = metaclass else { continue; }; - if metaclass.class.is_subclass_of(db, candidate.class) { - candidate = metaclass; + if metaclass.class.is_subclass_of(db, candidate.metaclass) { + candidate = MetaclassCandidate { + metaclass: metaclass.class, + explicit_metaclass_of: base_class.class, + }; continue; } - if candidate.class.is_subclass_of(db, metaclass.class) { + if candidate.metaclass.is_subclass_of(db, metaclass.class) { continue; } return Err(MetaclassError { kind: MetaclassErrorKind::Conflict { - metaclass1: candidate.class, - metaclass2: metaclass.class, + candidate1: candidate, + candidate2: MetaclassCandidate { + metaclass: metaclass.class, + explicit_metaclass_of: base_class.class, + }, + candidate1_is_base_class: explicit_metaclass.is_none(), }, }); } - Ok(Type::ClassLiteral(candidate)) + Ok(Type::ClassLiteral(ClassLiteralType { + class: candidate.metaclass, + })) } infer(db, self, &mut SeenSet::new(self)) @@ -2437,6 +2453,13 @@ impl<'db> Class<'db> { } } +/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct MetaclassCandidate<'db> { + metaclass: Class<'db>, + explicit_metaclass_of: Class<'db>, +} + /// A utility struct for detecting duplicates in class hierarchies while storing the initial /// entry on the stack. #[derive(Debug, Clone, PartialEq, Eq)] @@ -2541,9 +2564,18 @@ pub(super) enum MetaclassErrorKind<'db> { /// The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all /// its bases. Conflict { - metaclass1: Class<'db>, - metaclass2: Class<'db>, + /// `candidate1` will either be the explicit `metaclass=` keyword in the class definition, + /// or the inferred metaclass of a base class + candidate1: MetaclassCandidate<'db>, + + /// `candidate2` will always be the inferred metaclass of a base class + candidate2: MetaclassCandidate<'db>, + + /// Flag to indicate whether `candidate1` is the explicit `metaclass=` keyword or the + /// inferred metaclass of a base class. This helps us give better error messages in diagnostics. + candidate1_is_base_class: bool, }, + /// The class inherits from itself! /// /// This is very unlikely to happen in working real-world code, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index c716f6ff0b88f..f68f502e2557b 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -58,8 +58,8 @@ use crate::types::{ bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, typing_extensions_symbol, Boundness, BytesLiteralType, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, - KnownInstanceType, MetaclassErrorKind, SliceLiteralType, StringLiteralType, Symbol, Truthiness, - TupleType, Type, TypeArrayDisplay, UnionBuilder, UnionType, + KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, StringLiteralType, + Symbol, Truthiness, TupleType, Type, TypeArrayDisplay, UnionBuilder, UnionType, }; use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; @@ -520,18 +520,50 @@ impl<'db> TypeInferenceBuilder<'db> { if let Err(metaclass_error) = class.try_metaclass(self.db) { match metaclass_error.reason() { MetaclassErrorKind::Conflict { - metaclass1, - metaclass2 - } => self.diagnostics.add( - class.node(self.db).into(), - "conflicting-metaclass", - format_args!( - "The metaclass of a derived class (`{}`) must be a subclass of the metaclasses of all its bases, but `{}` and `{}` have no subclass relationship", - class.name(self.db), - metaclass1.name(self.db), - metaclass2.name(self.db), - ), - ), + candidate1: + MetaclassCandidate { + metaclass: metaclass1, + explicit_metaclass_of: class1, + }, + candidate2: + MetaclassCandidate { + metaclass: metaclass2, + explicit_metaclass_of: class2, + }, + candidate1_is_base_class, + } => { + let node = class.node(self.db).into(); + if *candidate1_is_base_class { + self.diagnostics.add( + node, + "conflicting-metaclass", + format_args!( + "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ + but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \ + have no subclass relationship", + class = class.name(self.db), + metaclass1 = metaclass1.name(self.db), + base1 = class1.name(self.db), + metaclass2 = metaclass2.name(self.db), + base2 = class2.name(self.db), + ) + ); + } else { + self.diagnostics.add( + node, + "conflicting-metaclass", + format_args!( + "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ + but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \ + have no subclass relationship", + class = class.name(self.db), + metaclass_of_class = metaclass1.name(self.db), + metaclass_of_base = metaclass2.name(self.db), + base = class2.name(self.db), + ) + ); + } + } MetaclassErrorKind::CyclicDefinition => { // Cyclic class definition diagnostic will already have been emitted above // in MRO calculation. From 1430f21283ece6da5971f4a01cf3a652398abdfe Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 8 Nov 2024 20:54:27 +0100 Subject: [PATCH 10/16] [red-knot] Fix `is_disjoint_from` for class literals (#14210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `Ty::BuiltinClassLiteral(…)` is a sub~~class~~type of `Ty::BuiltinInstance("type")`, so it can't be disjoint from it. ## Test Plan New `is_not_disjoint_from` test case --- crates/red_knot_python_semantic/src/types.rs | 23 +++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index a20a05c537d16..46a4d974755a3 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -823,14 +823,20 @@ impl<'db> Type<'db> { ), (Type::SliceLiteral(..), _) | (_, Type::SliceLiteral(..)) => true, - ( - Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..), - Type::Instance(InstanceType { class }), - ) - | ( - Type::Instance(InstanceType { class }), - Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..), - ) => !class.is_known(db, KnownClass::Object), + (Type::ClassLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::ClassLiteral(..)) => { + !matches!(class.known(db), Some(KnownClass::Type | KnownClass::Object)) + } + (Type::FunctionLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::FunctionLiteral(..)) => !matches!( + class.known(db), + Some(KnownClass::FunctionType | KnownClass::Object) + ), + (Type::ModuleLiteral(..), Type::Instance(InstanceType { class })) + | (Type::Instance(InstanceType { class }), Type::ModuleLiteral(..)) => !matches!( + class.known(db), + Some(KnownClass::ModuleType | KnownClass::Object) + ), (Type::Instance(..), Type::Instance(..)) => { // TODO: once we have support for `final`, there might be some cases where @@ -3048,6 +3054,7 @@ mod tests { #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(2), Ty::IntLiteral(3)]))] #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(2)], neg: vec![]}, Ty::IntLiteral(2))] #[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::BuiltinInstance("int")]))] + #[test_case(Ty::BuiltinClassLiteral("str"), Ty::BuiltinInstance("type"))] fn is_not_disjoint_from(a: Ty, b: Ty) { let db = setup_db(); let a = a.into_type(&db); From 645ce7e5ec068be09289d0dad84a0b6c27730f30 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 8 Nov 2024 13:23:05 -0800 Subject: [PATCH 11/16] [red-knot] infer types for PEP695 typevars (#14182) ## Summary Create definitions and infer types for PEP 695 type variables. This just gives us the type of the type variable itself (the type of `T` as a runtime object in the body of `def f[T](): ...`), with special handling for its attributes `__name__`, `__bound__`, `__constraints__`, and `__default__`. Mostly the support for these attributes exists because it is easy to implement and allows testing that we are internally representing the typevar correctly. This PR doesn't yet have support for interpreting a typevar as a type annotation, which is of course the primary use of a typevar. But the information we store in the typevar's type in this PR gives us everything we need to handle it correctly in a future PR when the typevar appears in an annotation. ## Test Plan Added mdtest. --- .../resources/mdtest/generics.md | 63 +++++- .../src/semantic_index/builder.rs | 5 + .../src/semantic_index/definition.rs | 59 +++++- crates/red_knot_python_semantic/src/types.rs | 200 +++++++++++++++--- .../src/types/display.rs | 5 + .../src/types/infer.rs | 139 +++++++++--- .../red_knot_python_semantic/src/types/mro.rs | 1 + .../test/corpus/27_func_generic_constraint.py | 3 + 8 files changed, 395 insertions(+), 80 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics.md b/crates/red_knot_python_semantic/resources/mdtest/generics.md index ff9f5ff8d165a..d4ccce2ae73bc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics.md @@ -6,13 +6,9 @@ Basic PEP 695 generics ```py class MyBox[T]: - # TODO: `T` is defined here - # error: [unresolved-reference] "Name `T` used when not defined" data: T box_model_number = 695 - # TODO: `T` is defined here - # error: [unresolved-reference] "Name `T` used when not defined" def __init__(self, data: T): self.data = data @@ -31,17 +27,12 @@ reveal_type(MyBox.box_model_number) # revealed: Literal[695] ```py class MyBox[T]: - # TODO: `T` is defined here - # error: [unresolved-reference] "Name `T` used when not defined" data: T - # TODO: `T` is defined here - # error: [unresolved-reference] "Name `T` used when not defined" def __init__(self, data: T): self.data = data -# TODO not error on the subscripting or the use of type param -# error: [unresolved-reference] "Name `T` used when not defined" +# TODO not error on the subscripting # error: [non-subscriptable] class MySecureBox[T](MyBox[T]): ... @@ -66,3 +57,55 @@ class S[T](Seq[S]): ... # error: [non-subscriptable] reveal_type(S) # revealed: Literal[S] ``` + +## Type params + +A PEP695 type variable defines a value of type `typing.TypeVar` with attributes `__name__`, +`__bounds__`, `__constraints__`, and `__default__` (the latter three all lazily evaluated): + +```py +def f[T, U: A, V: (A, B), W = A, X: A = A1](): + reveal_type(T) # revealed: TypeVar + reveal_type(T.__name__) # revealed: Literal["T"] + reveal_type(T.__bound__) # revealed: None + reveal_type(T.__constraints__) # revealed: tuple[()] + reveal_type(T.__default__) # revealed: NoDefault + + reveal_type(U) # revealed: TypeVar + reveal_type(U.__name__) # revealed: Literal["U"] + reveal_type(U.__bound__) # revealed: type[A] + reveal_type(U.__constraints__) # revealed: tuple[()] + reveal_type(U.__default__) # revealed: NoDefault + + reveal_type(V) # revealed: TypeVar + reveal_type(V.__name__) # revealed: Literal["V"] + reveal_type(V.__bound__) # revealed: None + reveal_type(V.__constraints__) # revealed: tuple[type[A], type[B]] + reveal_type(V.__default__) # revealed: NoDefault + + reveal_type(W) # revealed: TypeVar + reveal_type(W.__name__) # revealed: Literal["W"] + reveal_type(W.__bound__) # revealed: None + reveal_type(W.__constraints__) # revealed: tuple[()] + reveal_type(W.__default__) # revealed: type[A] + + reveal_type(X) # revealed: TypeVar + reveal_type(X.__name__) # revealed: Literal["X"] + reveal_type(X.__bound__) # revealed: type[A] + reveal_type(X.__constraints__) # revealed: tuple[()] + reveal_type(X.__default__) # revealed: type[A1] + +class A: ... +class B: ... +class A1(A): ... +``` + +## Minimum two constraints + +A typevar with less than two constraints emits a diagnostic and is treated as unconstrained: + +```py +# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types" +def f[T: (int,)](): + reveal_type(T.__constraints__) # revealed: tuple[()] +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index ba50e16148c34..fda1f07cbcdab 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -373,6 +373,11 @@ impl<'db> SemanticIndexBuilder<'db> { if let Some(default) = default { self.visit_expr(default); } + match type_param { + ast::TypeParam::TypeVar(node) => self.add_definition(symbol, node), + ast::TypeParam::ParamSpec(node) => self.add_definition(symbol, node), + ast::TypeParam::TypeVarTuple(node) => self.add_definition(symbol, node), + }; } } diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs index d165e521e8e88..11822e5d756a4 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/definition.rs @@ -92,6 +92,9 @@ pub(crate) enum DefinitionNodeRef<'a> { WithItem(WithItemDefinitionNodeRef<'a>), MatchPattern(MatchPatternDefinitionNodeRef<'a>), ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>), + TypeVar(&'a ast::TypeParamTypeVar), + ParamSpec(&'a ast::TypeParamParamSpec), + TypeVarTuple(&'a ast::TypeParamTypeVarTuple), } impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> { @@ -130,6 +133,24 @@ impl<'a> From<&'a ast::Alias> for DefinitionNodeRef<'a> { } } +impl<'a> From<&'a ast::TypeParamTypeVar> for DefinitionNodeRef<'a> { + fn from(value: &'a ast::TypeParamTypeVar) -> Self { + Self::TypeVar(value) + } +} + +impl<'a> From<&'a ast::TypeParamParamSpec> for DefinitionNodeRef<'a> { + fn from(value: &'a ast::TypeParamParamSpec) -> Self { + Self::ParamSpec(value) + } +} + +impl<'a> From<&'a ast::TypeParamTypeVarTuple> for DefinitionNodeRef<'a> { + fn from(value: &'a ast::TypeParamTypeVarTuple) -> Self { + Self::TypeVarTuple(value) + } +} + impl<'a> From> for DefinitionNodeRef<'a> { fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self { Self::ImportFrom(node_ref) @@ -317,6 +338,15 @@ impl<'db> DefinitionNodeRef<'db> { handler: AstNodeRef::new(parsed, handler), is_star, }), + DefinitionNodeRef::TypeVar(node) => { + DefinitionKind::TypeVar(AstNodeRef::new(parsed, node)) + } + DefinitionNodeRef::ParamSpec(node) => { + DefinitionKind::ParamSpec(AstNodeRef::new(parsed, node)) + } + DefinitionNodeRef::TypeVarTuple(node) => { + DefinitionKind::TypeVarTuple(AstNodeRef::new(parsed, node)) + } } } @@ -356,6 +386,9 @@ impl<'db> DefinitionNodeRef<'db> { identifier.into() } Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(), + Self::TypeVar(node) => node.into(), + Self::ParamSpec(node) => node.into(), + Self::TypeVarTuple(node) => node.into(), } } } @@ -412,6 +445,9 @@ pub enum DefinitionKind<'db> { WithItem(WithItemDefinitionKind), MatchPattern(MatchPatternDefinitionKind), ExceptHandler(ExceptHandlerDefinitionKind), + TypeVar(AstNodeRef), + ParamSpec(AstNodeRef), + TypeVarTuple(AstNodeRef), } impl DefinitionKind<'_> { @@ -421,7 +457,10 @@ impl DefinitionKind<'_> { DefinitionKind::Function(_) | DefinitionKind::Class(_) | DefinitionKind::Import(_) - | DefinitionKind::ImportFrom(_) => DefinitionCategory::DeclarationAndBinding, + | DefinitionKind::ImportFrom(_) + | DefinitionKind::TypeVar(_) + | DefinitionKind::ParamSpec(_) + | DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding, // a parameter always binds a value, but is only a declaration if annotated DefinitionKind::Parameter(parameter) => { if parameter.annotation.is_some() { @@ -696,3 +735,21 @@ impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey { Self(NodeKey::from_node(handler)) } } + +impl From<&ast::TypeParamTypeVar> for DefinitionNodeKey { + fn from(value: &ast::TypeParamTypeVar) -> Self { + Self(NodeKey::from_node(value)) + } +} + +impl From<&ast::TypeParamParamSpec> for DefinitionNodeKey { + fn from(value: &ast::TypeParamParamSpec) -> Self { + Self(NodeKey::from_node(value)) + } +} + +impl From<&ast::TypeParamTypeVarTuple> for DefinitionNodeKey { + fn from(value: &ast::TypeParamTypeVarTuple) -> Self { + Self(NodeKey::from_node(value)) + } +} diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 46a4d974755a3..947ccb801c1f2 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -307,7 +307,7 @@ fn declarations_ty<'db>( } /// Representation of a type: a set of possible values at runtime. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub enum Type<'db> { /// The dynamic type: a statically unknown set of values Any, @@ -336,7 +336,7 @@ pub enum Type<'db> { /// The set of Python objects with the given class in their __class__'s method resolution order Instance(InstanceType<'db>), /// A single Python object that requires special treatment in the type system - KnownInstance(KnownInstanceType), + KnownInstance(KnownInstanceType<'db>), /// The set of objects in any of the types in the union Union(UnionType<'db>), /// The set of objects in all of the types in the intersection @@ -657,17 +657,23 @@ impl<'db> Type<'db> { // TODO: Once we have support for final classes, we can establish that // `Type::SubclassOf('FinalClass')` is equivalent to `Type::ClassLiteral('FinalClass')`. - // TODO: The following is a workaround that is required to unify the two different - // versions of `NoneType` in typeshed. This should not be required anymore once we - // understand `sys.version_info` branches. + // TODO: The following is a workaround that is required to unify the two different versions + // of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once + // we understand `sys.version_info` branches. self == other || matches!((self, other), ( Type::Instance(InstanceType { class: self_class }), Type::Instance(InstanceType { class: target_class }) ) - if self_class.is_known(db, KnownClass::NoneType) && - target_class.is_known(db, KnownClass::NoneType)) + if ( + self_class.is_known(db, KnownClass::NoneType) && + target_class.is_known(db, KnownClass::NoneType) + ) || ( + self_class.is_known(db, KnownClass::NoDefaultType) && + target_class.is_known(db, KnownClass::NoDefaultType) + ) + ) } /// Return true if this type and `other` have no common elements. @@ -907,8 +913,11 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(..) | Type::KnownInstance(..) => true, Type::Instance(InstanceType { class }) => { - // TODO some more instance types can be singleton types (EllipsisType, NotImplementedType) - matches!(class.known(db), Some(KnownClass::NoneType)) + if let Some(known_class) = class.known(db) { + known_class.is_singleton() + } else { + false + } } Type::Tuple(..) => { // The empty tuple is a singleton on CPython and PyPy, but not on other Python @@ -961,7 +970,7 @@ impl<'db> Type<'db> { .all(|elem| elem.is_single_valued(db)), Type::Instance(InstanceType { class }) => match class.known(db) { - Some(KnownClass::NoneType) => true, + Some(KnownClass::NoneType | KnownClass::NoDefaultType) => true, Some( KnownClass::Bool | KnownClass::Object @@ -978,7 +987,8 @@ impl<'db> Type<'db> { | KnownClass::GenericAlias | KnownClass::ModuleType | KnownClass::FunctionType - | KnownClass::SpecialForm, + | KnownClass::SpecialForm + | KnownClass::TypeVar, ) => false, None => false, }, @@ -1049,9 +1059,7 @@ impl<'db> Type<'db> { } Type::ClassLiteral(class_ty) => class_ty.member(db, name), Type::SubclassOf(subclass_of_ty) => subclass_of_ty.member(db, name), - Type::KnownInstance(known_instance) => { - known_instance.instance_fallback(db).member(db, name) - } + Type::KnownInstance(known_instance) => known_instance.member(db, name), Type::Instance(_) => { // TODO MRO? get_own_instance_member, get_instance_member Type::Todo.into() @@ -1442,7 +1450,7 @@ impl<'db> Type<'db> { Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db), Type::StringLiteral(_) | Type::LiteralString => *self, Type::KnownInstance(known_instance) => { - Type::StringLiteral(StringLiteralType::new(db, known_instance.repr())) + Type::StringLiteral(StringLiteralType::new(db, known_instance.repr(db))) } // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), @@ -1464,7 +1472,7 @@ impl<'db> Type<'db> { })), Type::LiteralString => Type::LiteralString, Type::KnownInstance(known_instance) => { - Type::StringLiteral(StringLiteralType::new(db, known_instance.repr())) + Type::StringLiteral(StringLiteralType::new(db, known_instance.repr(db))) } // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), @@ -1514,7 +1522,10 @@ pub enum KnownClass { FunctionType, // Typeshed NoneType, // Part of `types` for Python >= 3.10 + // Typing SpecialForm, + TypeVar, + NoDefaultType, } impl<'db> KnownClass { @@ -1537,6 +1548,8 @@ impl<'db> KnownClass { Self::FunctionType => "FunctionType", Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", + Self::TypeVar => "TypeVar", + Self::NoDefaultType => "_NoDefaultType", } } @@ -1561,8 +1574,34 @@ impl<'db> KnownClass { Self::GenericAlias | Self::ModuleType | Self::FunctionType => { types_symbol(db, self.as_str()).unwrap_or_unknown() } - Self::SpecialForm => typing_symbol(db, self.as_str()).unwrap_or_unknown(), Self::NoneType => typeshed_symbol(db, self.as_str()).unwrap_or_unknown(), + Self::SpecialForm => typing_symbol(db, self.as_str()).unwrap_or_unknown(), + Self::TypeVar => typing_symbol(db, self.as_str()).unwrap_or_unknown(), + Self::NoDefaultType => typing_extensions_symbol(db, self.as_str()).unwrap_or_unknown(), + } + } + + fn is_singleton(self) -> bool { + // TODO there are other singleton types (EllipsisType, NotImplementedType) + match self { + Self::NoneType | Self::NoDefaultType => true, + Self::Bool + | Self::Object + | Self::Bytes + | Self::Tuple + | Self::Int + | Self::Float + | Self::Str + | Self::Set + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::SpecialForm + | Self::TypeVar => false, } } @@ -1597,6 +1636,7 @@ impl<'db> KnownClass { "ModuleType" => Some(Self::ModuleType), "FunctionType" => Some(Self::FunctionType), "_SpecialForm" => Some(Self::SpecialForm), + "_NoDefaultType" => Some(Self::NoDefaultType), _ => None, } } @@ -1621,7 +1661,7 @@ impl<'db> KnownClass { | Self::Slice => module.name() == "builtins", Self::GenericAlias | Self::ModuleType | Self::FunctionType => module.name() == "types", Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"), - Self::SpecialForm => { + Self::SpecialForm | Self::TypeVar | Self::NoDefaultType => { matches!(module.name().as_str(), "typing" | "typing_extensions") } } @@ -1629,17 +1669,20 @@ impl<'db> KnownClass { } /// Enumeration of specific runtime that are special enough to be considered their own type. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum KnownInstanceType { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum KnownInstanceType<'db> { /// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`) Literal, + /// A single instance of `typing.TypeVar` + TypeVar(TypeVarInstance<'db>), // TODO: fill this enum out with more special forms, etc. } -impl KnownInstanceType { +impl<'db> KnownInstanceType<'db> { pub const fn as_str(self) -> &'static str { match self { KnownInstanceType::Literal => "Literal", + KnownInstanceType::TypeVar(_) => "TypeVar", } } @@ -1647,13 +1690,15 @@ impl KnownInstanceType { pub const fn bool(self) -> Truthiness { match self { Self::Literal => Truthiness::AlwaysTrue, + Self::TypeVar(_) => Truthiness::AlwaysTrue, } } /// Return the repr of the symbol at runtime - pub const fn repr(self) -> &'static str { + pub fn repr(self, db: &'db dyn Db) -> &'db str { match self { Self::Literal => "typing.Literal", + Self::TypeVar(typevar) => typevar.name(db), } } @@ -1661,6 +1706,7 @@ impl KnownInstanceType { pub const fn class(self) -> KnownClass { match self { Self::Literal => KnownClass::SpecialForm, + Self::TypeVar(_) => KnownClass::TypeVar, } } @@ -1682,6 +1728,93 @@ impl KnownInstanceType { _ => None, } } + + fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + match (self, name) { + (Self::TypeVar(typevar), "__name__") => Symbol::Type( + Type::StringLiteral(StringLiteralType::new(db, typevar.name(db).as_str())), + Boundness::Bound, + ), + (Self::TypeVar(typevar), "__bound__") => Symbol::Type( + typevar + .upper_bound(db) + .map(|ty| ty.to_meta_type(db)) + .unwrap_or(KnownClass::NoneType.to_instance(db)), + Boundness::Bound, + ), + (Self::TypeVar(typevar), "__constraints__") => Symbol::Type( + Type::Tuple(TupleType::new( + db, + typevar + .constraints(db) + .map(|constraints| { + constraints + .iter() + .map(|ty| ty.to_meta_type(db)) + .collect::>() + }) + .unwrap_or_else(|| std::iter::empty().collect::>()), + )), + Boundness::Bound, + ), + (Self::TypeVar(typevar), "__default__") => Symbol::Type( + typevar + .default_ty(db) + .map(|ty| ty.to_meta_type(db)) + .unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db)), + Boundness::Bound, + ), + _ => self.instance_fallback(db).member(db, name), + } + } +} + +/// Data regarding a single type variable. +/// +/// This is referenced by `KnownInstanceType::TypeVar` (to represent the singleton type of the +/// runtime `typing.TypeVar` object itself). In the future, it will also be referenced also by a +/// new `Type` variant to represent the type that this typevar represents as an annotation: that +/// is, an unknown set of objects, constrained by the upper-bound/constraints on this type var, +/// defaulting to the default type of this type var when not otherwise bound to a type. +/// +/// This must be a tracked struct, not an interned one, because typevar equivalence is by identity, +/// not by value. Two typevars that have the same name, bound/constraints, and default, are still +/// different typevars: if used in the same scope, they may be bound to different types. +#[salsa::tracked] +pub struct TypeVarInstance<'db> { + /// The name of this TypeVar (e.g. `T`) + #[return_ref] + name: ast::name::Name, + + /// The upper bound or constraint on the type of this TypeVar + bound_or_constraints: Option>, + + /// The default type for this TypeVar + default_ty: Option>, +} + +impl<'db> TypeVarInstance<'db> { + pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { + if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { + Some(ty) + } else { + None + } + } + + pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&[Type<'db>]> { + if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { + Some(tuple.elements(db)) + } else { + None + } + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)] +pub enum TypeVarBoundOrConstraints<'db> { + UpperBound(Type<'db>), + Constraints(TupleType<'db>), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -2499,7 +2632,7 @@ impl SeenSet { } /// A singleton type representing a single class object at runtime. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct ClassLiteralType<'db> { class: Class<'db>, } @@ -2521,7 +2654,7 @@ impl<'db> From> for Type<'db> { } /// A type that represents `type[C]`, i.e. the class literal `C` and class literals that are subclasses of `C`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct SubclassOfType<'db> { class: Class<'db>, } @@ -2533,7 +2666,7 @@ impl<'db> SubclassOfType<'db> { } /// A type representing the set of runtime objects which are instances of a certain class. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] pub struct InstanceType<'db> { class: Class<'db>, } @@ -2746,9 +2879,10 @@ mod tests { // BuiltinInstance("str") corresponds to an instance of the builtin `str` class BuiltinInstance(&'static str), TypingInstance(&'static str), - KnownInstance(KnownInstanceType), + TypingLiteral, // BuiltinClassLiteral("str") corresponds to the builtin `str` class object itself BuiltinClassLiteral(&'static str), + KnownClassInstance(KnownClass), Union(Vec), Intersection { pos: Vec, neg: Vec }, Tuple(Vec), @@ -2769,8 +2903,9 @@ mod tests { Ty::BytesLiteral(s) => Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes())), Ty::BuiltinInstance(s) => builtins_symbol(db, s).expect_type().to_instance(db), Ty::TypingInstance(s) => typing_symbol(db, s).expect_type().to_instance(db), - Ty::KnownInstance(known_instance) => Type::KnownInstance(known_instance), + Ty::TypingLiteral => Type::KnownInstance(KnownInstanceType::Literal), Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).expect_type(), + Ty::KnownClassInstance(known_class) => known_class.to_instance(db), Ty::Union(tys) => { UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) } @@ -2876,14 +3011,8 @@ mod tests { #[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("str")], neg: vec![Ty::StringLiteral("foo")]}, Ty::Intersection{pos: vec![], neg: vec![Ty::IntLiteral(2)]})] #[test_case(Ty::BuiltinClassLiteral("int"), Ty::BuiltinClassLiteral("int"))] #[test_case(Ty::BuiltinClassLiteral("int"), Ty::BuiltinInstance("object"))] - #[test_case( - Ty::KnownInstance(KnownInstanceType::Literal), - Ty::TypingInstance("_SpecialForm") - )] - #[test_case( - Ty::KnownInstance(KnownInstanceType::Literal), - Ty::BuiltinInstance("object") - )] + #[test_case(Ty::TypingLiteral, Ty::TypingInstance("_SpecialForm"))] + #[test_case(Ty::TypingLiteral, Ty::BuiltinInstance("object"))] fn is_subtype_of(from: Ty, to: Ty) { let db = setup_db(); assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db))); @@ -3126,6 +3255,7 @@ mod tests { #[test_case(Ty::None)] #[test_case(Ty::BooleanLiteral(true))] #[test_case(Ty::BooleanLiteral(false))] + #[test_case(Ty::KnownClassInstance(KnownClass::NoDefaultType))] fn is_singleton(from: Ty) { let db = setup_db(); diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 6818fe12d6ea2..5bcafd902e6be 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -71,6 +71,11 @@ impl Display for DisplayRepresentation<'_> { { f.write_str("None") } + Type::Instance(InstanceType { class }) + if class.is_known(self.db, KnownClass::NoDefaultType) => + { + f.write_str("NoDefault") + } // `[Type::Todo]`'s display should be explicit that is not a valid display of // any other type Type::Todo => f.write_str("@Todo"), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index f68f502e2557b..30e9965e73409 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -59,7 +59,8 @@ use crate::types::{ Boundness, BytesLiteralType, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, StringLiteralType, - Symbol, Truthiness, TupleType, Type, TypeArrayDisplay, UnionBuilder, UnionType, + Symbol, Truthiness, TupleType, Type, TypeArrayDisplay, TypeVarBoundOrConstraints, + TypeVarInstance, UnionBuilder, UnionType, }; use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; @@ -642,6 +643,15 @@ impl<'db> TypeInferenceBuilder<'db> { DefinitionKind::ExceptHandler(except_handler_definition) => { self.infer_except_handler_definition(except_handler_definition, definition); } + DefinitionKind::TypeVar(node) => { + self.infer_typevar_definition(node, definition); + } + DefinitionKind::ParamSpec(node) => { + self.infer_paramspec_definition(node, definition); + } + DefinitionKind::TypeVarTuple(node) => { + self.infer_typevartuple_definition(node, definition); + } } } @@ -1352,6 +1362,82 @@ impl<'db> TypeInferenceBuilder<'db> { ); } + fn infer_typevar_definition( + &mut self, + node: &ast::TypeParamTypeVar, + definition: Definition<'db>, + ) { + let ast::TypeParamTypeVar { + range: _, + name, + bound, + default, + } = node; + let bound_or_constraint = match bound.as_deref() { + Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { + if elts.len() < 2 { + self.diagnostics.add( + expr.into(), + "invalid-typevar-constraints", + format_args!("TypeVar must have at least two constrained types"), + ); + self.infer_expression(expr); + None + } else { + let tuple = TupleType::new( + self.db, + elts.iter() + .map(|expr| self.infer_type_expression(expr)) + .collect::>(), + ); + let constraints = TypeVarBoundOrConstraints::Constraints(tuple); + self.store_expression_type(expr, Type::Tuple(tuple)); + Some(constraints) + } + } + Some(expr) => Some(TypeVarBoundOrConstraints::UpperBound( + self.infer_type_expression(expr), + )), + None => None, + }; + let default_ty = self.infer_optional_type_expression(default.as_deref()); + let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + self.db, + name.id.clone(), + bound_or_constraint, + default_ty, + ))); + self.add_declaration_with_binding(node.into(), definition, ty, ty); + } + + fn infer_paramspec_definition( + &mut self, + node: &ast::TypeParamParamSpec, + definition: Definition<'db>, + ) { + let ast::TypeParamParamSpec { + range: _, + name: _, + default, + } = node; + self.infer_optional_expression(default.as_deref()); + self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo); + } + + fn infer_typevartuple_definition( + &mut self, + node: &ast::TypeParamTypeVarTuple, + definition: Definition<'db>, + ) { + let ast::TypeParamTypeVarTuple { + range: _, + name: _, + default, + } = node; + self.infer_optional_expression(default.as_deref()); + self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo); + } + fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { let ast::StmtMatch { range: _, @@ -2013,13 +2099,6 @@ impl<'db> TypeInferenceBuilder<'db> { expression.map(|expr| self.infer_expression(expr)) } - fn infer_optional_annotation_expression( - &mut self, - expr: Option<&ast::Expr>, - ) -> Option> { - expr.map(|expr| self.infer_annotation_expression(expr)) - } - #[track_caller] fn infer_expression(&mut self, expression: &ast::Expr) -> Type<'db> { debug_assert_eq!( @@ -3912,32 +3991,9 @@ impl<'db> TypeInferenceBuilder<'db> { } = type_parameters; for type_param in type_params { match type_param { - ast::TypeParam::TypeVar(typevar) => { - let ast::TypeParamTypeVar { - range: _, - name: _, - bound, - default, - } = typevar; - self.infer_optional_expression(bound.as_deref()); - self.infer_optional_expression(default.as_deref()); - } - ast::TypeParam::ParamSpec(param_spec) => { - let ast::TypeParamParamSpec { - range: _, - name: _, - default, - } = param_spec; - self.infer_optional_expression(default.as_deref()); - } - ast::TypeParam::TypeVarTuple(typevar_tuple) => { - let ast::TypeParamTypeVarTuple { - range: _, - name: _, - default, - } = typevar_tuple; - self.infer_optional_expression(default.as_deref()); - } + ast::TypeParam::TypeVar(node) => self.infer_definition(node), + ast::TypeParam::ParamSpec(node) => self.infer_definition(node), + ast::TypeParam::TypeVarTuple(node) => self.infer_definition(node), } } } @@ -3971,6 +4027,13 @@ impl<'db> TypeInferenceBuilder<'db> { self.store_expression_type(expression, annotation_ty); annotation_ty } + + fn infer_optional_annotation_expression( + &mut self, + expr: Option<&ast::Expr>, + ) -> Option> { + expr.map(|expr| self.infer_annotation_expression(expr)) + } } /// Type expressions @@ -4145,6 +4208,13 @@ impl<'db> TypeInferenceBuilder<'db> { ty } + fn infer_optional_type_expression( + &mut self, + opt_expression: Option<&ast::Expr>, + ) -> Option> { + opt_expression.map(|expr| self.infer_type_expression(expr)) + } + /// Given the slice of a `tuple[]` annotation, return the type that the annotation represents fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Type<'db> { /// In most cases, if a subelement of the tuple is inferred as `Todo`, @@ -4262,6 +4332,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Unknown } }, + KnownInstanceType::TypeVar(_) => Type::Todo, } } diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index c30ad49a622af..254a6f03fd05c 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -381,6 +381,7 @@ impl<'db> ClassBase<'db> { | Type::SubclassOf(_) => None, Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::Literal => None, + KnownInstanceType::TypeVar(_) => None, }, } } diff --git a/crates/red_knot_workspace/resources/test/corpus/27_func_generic_constraint.py b/crates/red_knot_workspace/resources/test/corpus/27_func_generic_constraint.py index 8f5867f38cabd..763eea48276b2 100644 --- a/crates/red_knot_workspace/resources/test/corpus/27_func_generic_constraint.py +++ b/crates/red_knot_workspace/resources/test/corpus/27_func_generic_constraint.py @@ -1,2 +1,5 @@ def foo[T: (str, bytes)](x: T) -> T: ... + +def bar[T: (str,)](x: T) -> T: + ... From c0c4ae14ac3673906196e185060eaf23d33746cc Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 8 Nov 2024 13:37:25 -0800 Subject: [PATCH 12/16] [red-knot] make KnownClass::is_singleton a const fn (#14211) Follow-up from missed review comment on https://github.com/astral-sh/ruff/pull/14182 --- crates/red_knot_python_semantic/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 947ccb801c1f2..d43fe7a03f64b 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1581,7 +1581,7 @@ impl<'db> KnownClass { } } - fn is_singleton(self) -> bool { + const fn is_singleton(self) -> bool { // TODO there are other singleton types (EllipsisType, NotImplementedType) match self { Self::NoneType | Self::NoDefaultType => true, From de947deee77c85adcd3483dc76a2ceebcb438d24 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 8 Nov 2024 22:17:56 +0000 Subject: [PATCH 13/16] [red-knot] Consolidate detection of cyclically defined classes (#14207) --- crates/red_knot_python_semantic/src/types.rs | 221 ++++++++---------- .../src/types/infer.rs | 42 ++-- .../red_knot_python_semantic/src/types/mro.rs | 88 ++----- 3 files changed, 145 insertions(+), 206 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d43fe7a03f64b..fc7ffc47e3809 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2349,6 +2349,15 @@ impl<'db> Class<'db> { self.explicit_bases_query(db) } + /// Iterate over this class's explicit bases, filtering out any bases that are not class objects. + fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { + self.explicit_bases(db) + .iter() + .copied() + .filter_map(Type::into_class_literal) + .map(|ClassLiteralType { class }| class) + } + #[salsa::tracked(return_ref)] fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> { let class_stmt = self.node(db); @@ -2448,102 +2457,80 @@ impl<'db> Class<'db> { /// Return the metaclass of this class, or `Unknown` if the metaclass cannot be inferred. pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { // TODO: `type[Unknown]` would be a more precise fallback - // (needs support for ) self.try_metaclass(db).unwrap_or(Type::Unknown) } /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. #[salsa::tracked] pub(crate) fn try_metaclass(self, db: &'db dyn Db) -> Result, MetaclassError<'db>> { - /// Infer the metaclass of a class, tracking the classes that have been visited to detect - /// cyclic definitions. - fn infer<'db>( - db: &'db dyn Db, - class: Class<'db>, - seen: &mut SeenSet>, - ) -> Result, MetaclassError<'db>> { - // Recursively infer the metaclass of a class, ensuring that cyclic definitions are - // detected. - let mut safe_recurse = |class: Class<'db>| -> Result, MetaclassError<'db>> { - // Each base must be considered in isolation. - let num_seen = seen.len(); - if !seen.insert(class) { - return Err(MetaclassError { - kind: MetaclassErrorKind::CyclicDefinition, - }); - } - let metaclass = infer(db, class, seen)?; - seen.truncate(num_seen); - Ok(metaclass) - }; + // Identify the class's own metaclass (or take the first base class's metaclass). + let mut base_classes = self.fully_static_explicit_bases(db).peekable(); - let mut base_classes = class - .explicit_bases(db) - .iter() - .copied() - .filter_map(Type::into_class_literal); + if base_classes.peek().is_some() && self.is_cyclically_defined(db) { + // We emit diagnostics for cyclic class definitions elsewhere. + // Avoid attempting to infer the metaclass if the class is cyclically defined: + // it would be easy to enter an infinite loop. + // + // TODO: `type[Unknown]` might be better here? + return Ok(Type::Unknown); + } - // Identify the class's own metaclass (or take the first base class's metaclass). - let explicit_metaclass = class.explicit_metaclass(db); - let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass - { - (metaclass, class) - } else if let Some(base_class) = base_classes.next() { - (safe_recurse(base_class.class)?, base_class.class) - } else { - (KnownClass::Type.to_class(db), class) - }; + let explicit_metaclass = self.explicit_metaclass(db); + let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { + (metaclass, self) + } else if let Some(base_class) = base_classes.next() { + (base_class.metaclass(db), base_class) + } else { + (KnownClass::Type.to_class(db), self) + }; - let mut candidate = if let Type::ClassLiteral(metaclass_ty) = metaclass { - MetaclassCandidate { - metaclass: metaclass_ty.class, - explicit_metaclass_of: class_metaclass_was_from, - } - } else { - // TODO: If the metaclass is not a class, we should verify that it's a callable - // which accepts the same arguments as `type.__new__` (otherwise error), and return - // the meta-type of its return type. (And validate that is a class type?) - return Ok(Type::Todo); - }; + let mut candidate = if let Type::ClassLiteral(metaclass_ty) = metaclass { + MetaclassCandidate { + metaclass: metaclass_ty.class, + explicit_metaclass_of: class_metaclass_was_from, + } + } else { + // TODO: If the metaclass is not a class, we should verify that it's a callable + // which accepts the same arguments as `type.__new__` (otherwise error), and return + // the meta-type of its return type. (And validate that is a class type?) + return Ok(Type::Todo); + }; - // Reconcile all base classes' metaclasses with the candidate metaclass. - // - // See: - // - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass - // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 - for base_class in base_classes { - let metaclass = safe_recurse(base_class.class)?; - let Type::ClassLiteral(metaclass) = metaclass else { - continue; + // Reconcile all base classes' metaclasses with the candidate metaclass. + // + // See: + // - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass + // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 + for base_class in base_classes { + let metaclass = base_class.metaclass(db); + let Type::ClassLiteral(metaclass) = metaclass else { + continue; + }; + if metaclass.class.is_subclass_of(db, candidate.metaclass) { + candidate = MetaclassCandidate { + metaclass: metaclass.class, + explicit_metaclass_of: base_class, }; - if metaclass.class.is_subclass_of(db, candidate.metaclass) { - candidate = MetaclassCandidate { + continue; + } + if candidate.metaclass.is_subclass_of(db, metaclass.class) { + continue; + } + return Err(MetaclassError { + kind: MetaclassErrorKind::Conflict { + candidate1: candidate, + candidate2: MetaclassCandidate { metaclass: metaclass.class, - explicit_metaclass_of: base_class.class, - }; - continue; - } - if candidate.metaclass.is_subclass_of(db, metaclass.class) { - continue; - } - return Err(MetaclassError { - kind: MetaclassErrorKind::Conflict { - candidate1: candidate, - candidate2: MetaclassCandidate { - metaclass: metaclass.class, - explicit_metaclass_of: base_class.class, - }, - candidate1_is_base_class: explicit_metaclass.is_none(), + explicit_metaclass_of: base_class, }, - }); - } - - Ok(Type::ClassLiteral(ClassLiteralType { - class: candidate.metaclass, - })) + candidate1_is_base_class: explicit_metaclass.is_none(), + }, + }); } - infer(db, self, &mut SeenSet::new(self)) + Ok(Type::ClassLiteral(ClassLiteralType { + class: candidate.metaclass, + })) } /// Returns the class member of this class named `name`. @@ -2590,6 +2577,39 @@ impl<'db> Class<'db> { let scope = self.body_scope(db); symbol(db, scope, name) } + + /// Return `true` if this class appears to be a cyclic definition, + /// i.e., it inherits either directly or indirectly from itself. + /// + /// A class definition like this will fail at runtime, + /// but we must be resilient to it or we could panic. + #[salsa::tracked] + fn is_cyclically_defined(self, db: &'db dyn Db) -> bool { + fn is_cyclically_defined_recursive<'db>( + db: &'db dyn Db, + class: Class<'db>, + classes_to_watch: &mut IndexSet>, + ) -> bool { + if !classes_to_watch.insert(class) { + return true; + } + for explicit_base_class in class.fully_static_explicit_bases(db) { + // Each base must be considered in isolation. + // This is due to the fact that if a class uses multiple inheritance, + // there could easily be a situation where two bases have the same class in their MROs; + // that isn't enough to constitute the class being cyclically defined. + let classes_to_watch_len = classes_to_watch.len(); + if is_cyclically_defined_recursive(db, explicit_base_class, classes_to_watch) { + return true; + } + classes_to_watch.truncate(classes_to_watch_len); + } + false + } + + self.fully_static_explicit_bases(db) + .any(|base_class| is_cyclically_defined_recursive(db, base_class, &mut IndexSet::new())) + } } /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. @@ -2599,38 +2619,6 @@ pub(super) struct MetaclassCandidate<'db> { explicit_metaclass_of: Class<'db>, } -/// A utility struct for detecting duplicates in class hierarchies while storing the initial -/// entry on the stack. -#[derive(Debug, Clone, PartialEq, Eq)] -struct SeenSet { - initial: T, - visited: IndexSet, -} - -impl SeenSet { - fn new(initial: T) -> SeenSet { - Self { - initial, - visited: IndexSet::new(), - } - } - - fn len(&self) -> usize { - self.visited.len() - } - - fn truncate(&mut self, len: usize) { - self.visited.truncate(len); - } - - fn insert(&mut self, value: T) -> bool { - if value == self.initial { - return false; - } - self.visited.insert(value) - } -} - /// A singleton type representing a single class object at runtime. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct ClassLiteralType<'db> { @@ -2714,13 +2702,6 @@ pub(super) enum MetaclassErrorKind<'db> { /// inferred metaclass of a base class. This helps us give better error messages in diagnostics. candidate1_is_base_class: bool, }, - - /// The class inherits from itself! - /// - /// This is very unlikely to happen in working real-world code, - /// but it's important to explicitly account for it. - /// If we don't, there's a possibility of an infinite loop and a panic. - CyclicDefinition, } #[salsa::interned] diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 30e9965e73409..7f6fbdd6bd496 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -457,9 +457,13 @@ impl<'db> TypeInferenceBuilder<'db> { self.check_class_definitions(); } - /// Iterate over all class definitions to check that Python will be able to create a - /// consistent "[method resolution order]" and [metaclass] for each class at runtime. If not, - /// issue a diagnostic. + /// Iterate over all class definitions to check that the definition will not cause an exception + /// to be raised at runtime. This needs to be done after most other types in the scope have been + /// inferred, due to the fact that base classes can be deferred. If it looks like a class + /// definition is invalid in some way, issue a diagnostic. + /// + /// Among the things we check for in this method are whether Python will be able to determine a + /// consistent "[method resolution order]" and [metaclass] for each class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order /// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses @@ -471,7 +475,25 @@ impl<'db> TypeInferenceBuilder<'db> { .filter_map(|ty| ty.into_class_literal()) .map(|class_ty| class_ty.class); + // Iterate through all class definitions in this scope. for class in class_definitions { + // (1) Check that the class does not have a cyclic definition + if class.is_cyclically_defined(self.db) { + self.diagnostics.add( + class.node(self.db).into(), + "cyclic-class-def", + format_args!( + "Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)", + class.name(self.db), + class.name(self.db) + ), + ); + // Attempting to determine the MRO of a class or if the class has a metaclass conflict + // is impossible if the class is cyclically defined; there's nothing more to do here. + continue; + } + + // (2) Check that the class's MRO is resolvable if let Err(mro_error) = class.try_mro(self.db).as_ref() { match mro_error.reason() { MroErrorKind::DuplicateBases(duplicates) => { @@ -484,15 +506,6 @@ impl<'db> TypeInferenceBuilder<'db> { ); } } - MroErrorKind::CyclicClassDefinition => self.diagnostics.add( - class.node(self.db).into(), - "cyclic-class-def", - format_args!( - "Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)", - class.name(self.db), - class.name(self.db) - ), - ), MroErrorKind::InvalidBases(bases) => { let base_nodes = class.node(self.db).bases(); for (index, base_ty) in bases { @@ -518,6 +531,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } + // (3) Check that the class's metaclass can be determined without error. if let Err(metaclass_error) = class.try_metaclass(self.db) { match metaclass_error.reason() { MetaclassErrorKind::Conflict { @@ -565,10 +579,6 @@ impl<'db> TypeInferenceBuilder<'db> { ); } } - MetaclassErrorKind::CyclicDefinition => { - // Cyclic class definition diagnostic will already have been emitted above - // in MRO calculation. - } } } } diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index 254a6f03fd05c..8ff2279e36b57 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -1,7 +1,6 @@ use std::collections::VecDeque; use std::ops::Deref; -use indexmap::IndexSet; use itertools::Either; use rustc_hash::FxHashSet; @@ -26,22 +25,30 @@ impl<'db> Mro<'db> { /// (We emit a diagnostic warning about the runtime `TypeError` in /// [`super::infer::TypeInferenceBuilder::infer_region_scope`].) pub(super) fn of_class(db: &'db dyn Db, class: Class<'db>) -> Result> { - Self::of_class_impl(db, class).map_err(|error_kind| { - let fallback_mro = Self::from([ - ClassBase::Class(class), - ClassBase::Unknown, - ClassBase::object(db), - ]); - MroError { - kind: error_kind, - fallback_mro, - } + Self::of_class_impl(db, class).map_err(|error_kind| MroError { + kind: error_kind, + fallback_mro: Self::from_error(db, class), }) } + pub(super) fn from_error(db: &'db dyn Db, class: Class<'db>) -> Self { + Self::from([ + ClassBase::Class(class), + ClassBase::Unknown, + ClassBase::object(db), + ]) + } + fn of_class_impl(db: &'db dyn Db, class: Class<'db>) -> Result> { let class_bases = class.explicit_bases(db); + if !class_bases.is_empty() && class.is_cyclically_defined(db) { + // We emit errors for cyclically defined classes elsewhere. + // It's important that we don't even try to infer the MRO for a cyclically defined class, + // or we'll end up in an infinite loop. + return Ok(Mro::from_error(db, class)); + } + match class_bases { // `builtins.object` is the special case: // the only class in Python that has an MRO with length <2 @@ -77,11 +84,6 @@ impl<'db> Mro<'db> { Err(MroErrorKind::InvalidBases(bases_info)) }, |single_base| { - if let ClassBase::Class(class_base) = single_base { - if class_is_cyclically_defined(db, class_base) { - return Err(MroErrorKind::CyclicClassDefinition); - } - } let mro = std::iter::once(ClassBase::Class(class)) .chain(single_base.mro(db)) .collect(); @@ -96,10 +98,6 @@ impl<'db> Mro<'db> { // what MRO Python will give this class at runtime // (if an MRO is indeed resolvable at all!) multiple_bases => { - if class_is_cyclically_defined(db, class) { - return Err(MroErrorKind::CyclicClassDefinition); - } - let mut valid_bases = vec![]; let mut invalid_bases = vec![]; @@ -282,13 +280,6 @@ pub(super) enum MroErrorKind<'db> { /// Each index is the index of a node representing an invalid base. InvalidBases(Box<[(usize, Type<'db>)]>), - /// The class inherits from itself! - /// - /// This is very unlikely to happen in working real-world code, - /// but it's important to explicitly account for it. - /// If we don't, there's a possibility of an infinite loop and a panic. - CyclicClassDefinition, - /// The class has one or more duplicate bases. /// /// This variant records the indices and [`Class`]es @@ -461,46 +452,3 @@ fn c3_merge(mut sequences: Vec>) -> Option { } } } - -/// Return `true` if this class appears to be a cyclic definition, -/// i.e., it inherits either directly or indirectly from itself. -/// -/// A class definition like this will fail at runtime, -/// but we must be resilient to it or we could panic. -fn class_is_cyclically_defined(db: &dyn Db, class: Class) -> bool { - fn is_cyclically_defined_recursive<'db>( - db: &'db dyn Db, - class: Class<'db>, - classes_to_watch: &mut IndexSet>, - ) -> bool { - if !classes_to_watch.insert(class) { - return true; - } - for explicit_base_class in class - .explicit_bases(db) - .iter() - .copied() - .filter_map(Type::into_class_literal) - .map(|ClassLiteralType { class }| class) - { - // Each base must be considered in isolation. - // This is due to the fact that if a class uses multiple inheritance, - // there could easily be a situation where two bases have the same class in their MROs; - // that isn't enough to constitute the class being cyclically defined. - let classes_to_watch_len = classes_to_watch.len(); - if is_cyclically_defined_recursive(db, explicit_base_class, classes_to_watch) { - return true; - } - classes_to_watch.truncate(classes_to_watch_len); - } - false - } - - class - .explicit_bases(db) - .iter() - .copied() - .filter_map(Type::into_class_literal) - .map(|ClassLiteralType { class }| class) - .any(|base_class| is_cyclically_defined_recursive(db, base_class, &mut IndexSet::default())) -} From b19f38824967645afdc99770caba1a635b2d2384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Sat, 9 Nov 2024 02:54:18 +0100 Subject: [PATCH 14/16] [`refurb`] Use `UserString` instead of non-existent `UserStr` (#14209) ## Summary The class name is UserString, not a UserStr, see https://docs.python.org/3.9/library/collections.html#collections.UserString --- .../src/rules/refurb/rules/subclass_builtin.rs | 4 ++-- ..._linter__rules__refurb__tests__FURB189_FURB189.py.snap | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs b/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs index c560156dff175..41575b3d341ba 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs @@ -10,7 +10,7 @@ use crate::{checkers::ast::Checker, importer::ImportRequest}; /// /// ## Why is this bad? /// Subclassing `dict`, `list`, or `str` objects can be error prone, use the -/// `UserDict`, `UserList`, and `UserStr` objects from the `collections` module +/// `UserDict`, `UserList`, and `UserString` objects from the `collections` module /// instead. /// /// ## Example @@ -124,7 +124,7 @@ impl SupportedBuiltins { match self { SupportedBuiltins::Dict => "UserDict", SupportedBuiltins::List => "UserList", - SupportedBuiltins::Str => "UserStr", + SupportedBuiltins::Str => "UserString", } } } diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB189_FURB189.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB189_FURB189.py.snap index 5378d2df19ca2..ca530033a83cf 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB189_FURB189.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB189_FURB189.py.snap @@ -48,7 +48,7 @@ FURB189.py:20:9: FURB189 [*] Subclassing `list` can be error prone, use `collect 22 22 | 23 23 | class S(str): -FURB189.py:23:9: FURB189 [*] Subclassing `str` can be error prone, use `collections.UserStr` instead +FURB189.py:23:9: FURB189 [*] Subclassing `str` can be error prone, use `collections.UserString` instead | 21 | pass 22 | @@ -56,13 +56,13 @@ FURB189.py:23:9: FURB189 [*] Subclassing `str` can be error prone, use `collecti | ^^^ FURB189 24 | pass | - = help: Replace with `collections.UserStr` + = help: Replace with `collections.UserString` ℹ Unsafe fix 1 1 | # setup 2 2 | from enum import Enum, EnumMeta 3 |-from collections import UserList as UL - 3 |+from collections import UserList as UL, UserStr + 3 |+from collections import UserList as UL, UserString 4 4 | 5 5 | class SetOnceMappingMixin: 6 6 | __slots__ = () @@ -71,7 +71,7 @@ FURB189.py:23:9: FURB189 [*] Subclassing `str` can be error prone, use `collecti 21 21 | pass 22 22 | 23 |-class S(str): - 23 |+class S(UserStr): + 23 |+class S(UserString): 24 24 | pass 25 25 | 26 26 | # currently not detected From 93fdf7ed364a3c2987afcf32022c79156bc9286f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 8 Nov 2024 21:07:13 -0500 Subject: [PATCH 15/16] Fix miscellaneous issues in `await-outside-async detection` (#14218) ## Summary Closes https://github.com/astral-sh/ruff/issues/14167. --- .../fixtures/pylint/await_outside_async.py | 44 +++++++++++++ .../src/checkers/ast/analyze/comprehension.rs | 7 ++- .../src/checkers/ast/analyze/statement.rs | 20 +++++- crates/ruff_linter/src/checkers/ast/mod.rs | 28 +++++---- .../rules/pylint/rules/await_outside_async.rs | 37 ++++++++--- ...tests__PLE1142_await_outside_async.py.snap | 62 ++++++++++++++++--- crates/ruff_python_semantic/src/scope.rs | 10 ++- 7 files changed, 177 insertions(+), 31 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py b/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py index 2bc1267615a4e..f652428a58f1f 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py @@ -1,13 +1,16 @@ # pylint: disable=missing-docstring,unused-variable import asyncio + async def nested(): return 42 + async def main(): nested() print(await nested()) # This is okay + def not_async(): print(await nested()) # [await-outside-async] @@ -15,6 +18,7 @@ def not_async(): async def func(i): return i**2 + async def okay_function(): var = [await func(i) for i in range(5)] # This should be okay @@ -28,3 +32,43 @@ def inner_func(): def outer_func(): async def inner_func(): await asyncio.sleep(1) + + +def async_for_loop(): + async for x in foo(): + pass + + +def async_with(): + async with foo(): + pass + + +# See: https://github.com/astral-sh/ruff/issues/14167 +def async_for_generator_elt(): + (await x for x in foo()) + + +# See: https://github.com/astral-sh/ruff/issues/14167 +def async_for_list_comprehension_elt(): + [await x for x in foo()] + + +# See: https://github.com/astral-sh/ruff/issues/14167 +def async_for_list_comprehension(): + [x async for x in foo()] + + +# See: https://github.com/astral-sh/ruff/issues/14167 +def await_generator_iter(): + (x for x in await foo()) + + +# See: https://github.com/astral-sh/ruff/issues/14167 +def await_generator_target(): + (x async for x in foo()) + + +# See: https://github.com/astral-sh/ruff/issues/14167 +def async_for_list_comprehension_target(): + [x for x in await foo()] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs b/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs index 821e62c283597..b136791652793 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs @@ -2,7 +2,7 @@ use ruff_python_ast::Comprehension; use crate::checkers::ast::Checker; use crate::codes::Rule; -use crate::rules::{flake8_simplify, refurb}; +use crate::rules::{flake8_simplify, pylint, refurb}; /// Run lint rules over a [`Comprehension`] syntax nodes. pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker) { @@ -12,4 +12,9 @@ pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker if checker.enabled(Rule::ReadlinesInFor) { refurb::rules::readlines_in_comprehension(checker, comprehension); } + if comprehension.is_async { + if checker.enabled(Rule::AwaitOutsideAsync) { + pylint::rules::await_outside_async(checker, comprehension); + } + } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index e26d9e8cb3f7c..6624dd8d14479 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1303,7 +1303,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { ruff::rules::assert_with_print_message(checker, assert_stmt); } } - Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => { + Stmt::With( + with_stmt @ ast::StmtWith { + items, + body, + is_async, + .. + }, + ) => { if checker.enabled(Rule::TooManyNestedBlocks) { pylint::rules::too_many_nested_blocks(checker, stmt); } @@ -1335,6 +1342,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::CancelScopeNoCheckpoint) { flake8_async::rules::cancel_scope_no_checkpoint(checker, with_stmt, items); } + if *is_async { + if checker.enabled(Rule::AwaitOutsideAsync) { + pylint::rules::await_outside_async(checker, stmt); + } + } } Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => { if checker.enabled(Rule::TooManyNestedBlocks) { @@ -1422,7 +1434,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::ReadlinesInFor) { refurb::rules::readlines_in_for(checker, for_stmt); } - if !is_async { + if *is_async { + if checker.enabled(Rule::AwaitOutsideAsync) { + pylint::rules::await_outside_async(checker, stmt); + } + } else { if checker.enabled(Rule::ReimplementedBuiltin) { flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt); } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 9e69dc137cb25..86852bf9502e3 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -53,9 +53,9 @@ use ruff_python_parser::{Parsed, Tokens}; use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags}; use ruff_python_semantic::analyze::{imports, typing}; use ruff_python_semantic::{ - BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module, - ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, - StarImport, SubmoduleImport, + BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, GeneratorKind, Globals, + Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel, + SemanticModelFlags, StarImport, SubmoduleImport, }; use ruff_python_stdlib::builtins::{python_builtins, MAGIC_GLOBALS}; use ruff_python_trivia::CommentRanges; @@ -1137,19 +1137,25 @@ impl<'a> Visitor<'a> for Checker<'a> { elt, generators, range: _, - }) - | Expr::SetComp(ast::ExprSetComp { + }) => { + self.visit_generators(GeneratorKind::ListComprehension, generators); + self.visit_expr(elt); + } + Expr::SetComp(ast::ExprSetComp { elt, generators, range: _, - }) - | Expr::Generator(ast::ExprGenerator { + }) => { + self.visit_generators(GeneratorKind::SetComprehension, generators); + self.visit_expr(elt); + } + Expr::Generator(ast::ExprGenerator { elt, generators, range: _, parenthesized: _, }) => { - self.visit_generators(generators); + self.visit_generators(GeneratorKind::Generator, generators); self.visit_expr(elt); } Expr::DictComp(ast::ExprDictComp { @@ -1158,7 +1164,7 @@ impl<'a> Visitor<'a> for Checker<'a> { generators, range: _, }) => { - self.visit_generators(generators); + self.visit_generators(GeneratorKind::DictComprehension, generators); self.visit_expr(key); self.visit_expr(value); } @@ -1748,7 +1754,7 @@ impl<'a> Checker<'a> { /// Visit a list of [`Comprehension`] nodes, assumed to be the comprehensions that compose a /// generator expression, like a list or set comprehension. - fn visit_generators(&mut self, generators: &'a [Comprehension]) { + fn visit_generators(&mut self, kind: GeneratorKind, generators: &'a [Comprehension]) { let mut iterator = generators.iter(); let Some(generator) = iterator.next() else { @@ -1785,7 +1791,7 @@ impl<'a> Checker<'a> { // while all subsequent reads and writes are evaluated in the inner scope. In particular, // `x` is local to `foo`, and the `T` in `y=T` skips the class scope when resolving. self.visit_expr(&generator.iter); - self.semantic.push_scope(ScopeKind::Generator); + self.semantic.push_scope(ScopeKind::Generator(kind)); self.semantic.flags = flags | SemanticModelFlags::COMPREHENSION_ASSIGNMENT; self.visit_expr(&generator.target); diff --git a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs index 61af369ea4e08..db248c1911916 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs @@ -1,16 +1,15 @@ -use ruff_python_ast::Expr; - use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::{GeneratorKind, ScopeKind}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for uses of `await` outside of `async` functions. +/// Checks for uses of `await` outside `async` functions. /// /// ## Why is this bad? -/// Using `await` outside of an `async` function is a syntax error. +/// Using `await` outside an `async` function is a syntax error. /// /// ## Example /// ```python @@ -44,10 +43,30 @@ impl Violation for AwaitOutsideAsync { } /// PLE1142 -pub(crate) fn await_outside_async(checker: &mut Checker, expr: &Expr) { - if !checker.semantic().in_async_context() { - checker - .diagnostics - .push(Diagnostic::new(AwaitOutsideAsync, expr.range())); +pub(crate) fn await_outside_async(checker: &mut Checker, node: T) { + // If we're in an `async` function, we're good. + if checker.semantic().in_async_context() { + return; + } + + // Generators are evaluated lazily, so you can use `await` in them. For example: + // ```python + // # This is valid + // (await x for x in y) + // (x async for x in y) + // + // # This is invalid + // (x for x in async y) + // [await x for x in y] + // ``` + if matches!( + checker.semantic().current_scope().kind, + ScopeKind::Generator(GeneratorKind::Generator) + ) { + return; } + + checker + .diagnostics + .push(Diagnostic::new(AwaitOutsideAsync, node.range())); } diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap index a1f4507c82974..ce1a9a7d388fc 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap @@ -1,19 +1,67 @@ --- source: crates/ruff_linter/src/rules/pylint/mod.rs --- -await_outside_async.py:12:11: PLE1142 `await` should be used within an async function +await_outside_async.py:15:11: PLE1142 `await` should be used within an async function | -11 | def not_async(): -12 | print(await nested()) # [await-outside-async] +14 | def not_async(): +15 | print(await nested()) # [await-outside-async] | ^^^^^^^^^^^^^^ PLE1142 | -await_outside_async.py:25:9: PLE1142 `await` should be used within an async function +await_outside_async.py:29:9: PLE1142 `await` should be used within an async function | -23 | async def func2(): -24 | def inner_func(): -25 | await asyncio.sleep(1) # [await-outside-async] +27 | async def func2(): +28 | def inner_func(): +29 | await asyncio.sleep(1) # [await-outside-async] | ^^^^^^^^^^^^^^^^^^^^^^ PLE1142 | +await_outside_async.py:38:5: PLE1142 `await` should be used within an async function + | +37 | def async_for_loop(): +38 | async for x in foo(): + | _____^ +39 | | pass + | |____________^ PLE1142 + | + +await_outside_async.py:43:5: PLE1142 `await` should be used within an async function + | +42 | def async_with(): +43 | async with foo(): + | _____^ +44 | | pass + | |____________^ PLE1142 + | + +await_outside_async.py:54:6: PLE1142 `await` should be used within an async function + | +52 | # See: https://github.com/astral-sh/ruff/issues/14167 +53 | def async_for_list_comprehension_elt(): +54 | [await x for x in foo()] + | ^^^^^^^ PLE1142 + | +await_outside_async.py:59:8: PLE1142 `await` should be used within an async function + | +57 | # See: https://github.com/astral-sh/ruff/issues/14167 +58 | def async_for_list_comprehension(): +59 | [x async for x in foo()] + | ^^^^^^^^^^^^^^^^^^^^ PLE1142 + | + +await_outside_async.py:64:17: PLE1142 `await` should be used within an async function + | +62 | # See: https://github.com/astral-sh/ruff/issues/14167 +63 | def await_generator_iter(): +64 | (x for x in await foo()) + | ^^^^^^^^^^^ PLE1142 + | + +await_outside_async.py:74:17: PLE1142 `await` should be used within an async function + | +72 | # See: https://github.com/astral-sh/ruff/issues/14167 +73 | def async_for_list_comprehension_target(): +74 | [x for x in await foo()] + | ^^^^^^^^^^^ PLE1142 + | diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index a7a69fd555390..9f610eba007b3 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -170,13 +170,21 @@ bitflags! { pub enum ScopeKind<'a> { Class(&'a ast::StmtClassDef), Function(&'a ast::StmtFunctionDef), - Generator, + Generator(GeneratorKind), Module, /// A Python 3.12+ [annotation scope](https://docs.python.org/3/reference/executionmodel.html#annotation-scopes) Type, Lambda(&'a ast::ExprLambda), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GeneratorKind { + Generator, + ListComprehension, + DictComprehension, + SetComprehension, +} + /// Id uniquely identifying a scope in a program. /// /// Using a `u32` is sufficient because Ruff only supports parsing documents with a size of max `u32::max` From b8dc780bdced4df923a6eb1d415ffafc2a25fdfb Mon Sep 17 00:00:00 2001 From: Dylan <53534755+dylwil3@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:08:22 -0600 Subject: [PATCH 16/16] [`refurb`] Further special cases added to `verbose-decimal-constructor (FURB157)` (#14216) This PR accounts for further subtleties in `Decimal` parsing: - Strings which are empty modulo underscores and surrounding whitespace are skipped - `Decimal("-0")` is skipped - `Decimal("{integer literal that is longer than 640 digits}")` are skipped (see linked issue for explanation) NB: The snapshot did not need to be updated since the new test cases are "Ok" instances and added below the diff. Closes #14204 --- .../resources/test/fixtures/refurb/FURB157.py | 9 +++- .../rules/verbose_decimal_constructor.rs | 45 +++++++++---------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py index 56c1f00aba20d..facb2a373568f 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py @@ -34,4 +34,11 @@ # Ok: even though this is equal to `Decimal(123)`, # we assume that a developer would # only write it this way if they meant to. -Decimal("١٢٣") \ No newline at end of file +Decimal("١٢٣") + +# Further subtleties +# https://github.com/astral-sh/ruff/issues/14204 +Decimal("-0") # Ok +Decimal("_") # Ok +Decimal(" ") # Ok +Decimal("10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") # Ok diff --git a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs index 6aecfadbd4499..559458cc57ef8 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs @@ -94,6 +94,12 @@ pub(crate) fn verbose_decimal_constructor(checker: &mut Checker, call: &ast::Exp ("", trimmed) }; + // Early return if we now have an empty string + // or a very long string: + if (rest.len() > PYTHONINTMAXSTRDIGITS) || (rest.len() == 0) { + return; + } + // Skip leading zeros. let rest = rest.trim_start_matches('0'); @@ -103,7 +109,15 @@ pub(crate) fn verbose_decimal_constructor(checker: &mut Checker, call: &ast::Exp }; // If all the characters are zeros, then the value is zero. - let rest = if rest.is_empty() { "0" } else { rest }; + let rest = match (unary, rest.is_empty()) { + // `Decimal("-0")` is not the same as `Decimal("0")` + // so we return early. + ("-", true) => { + return; + } + (_, true) => "0", + _ => rest, + }; let replacement = format!("{unary}{rest}"); let mut diagnostic = Diagnostic::new( @@ -168,25 +182,10 @@ pub(crate) fn verbose_decimal_constructor(checker: &mut Checker, call: &ast::Exp checker.diagnostics.push(diagnostic); } -// // Slightly modified from [CPython regex] to ignore https://github.com/python/cpython/blob/ac556a2ad1213b8bb81372fe6fb762f5fcb076de/Lib/_pydecimal.py#L6060-L6077 -// static DECIMAL_PARSER_REGEX: LazyLock = LazyLock::new(|| { -// Regex::new( -// r"(?x) # Verbose mode for comments -// ^ # Start of string -// (?P[-+])? # Optional sign -// (?: -// (?P\d*) # Integer part (can be empty) -// (\.(?P\d+))? # Optional fractional part -// (E(?P[-+]?\d+))? # Optional exponent -// | -// Inf(inity)? # Infinity -// | -// (?Ps)? # Optional signal -// NaN # NaN -// (?P\d*) # Optional diagnostic info -// ) -// $ # End of string -// ", -// ) -// .unwrap() -// }); +// ```console +// $ python +// >>> import sys +// >>> sys.int_info.str_digits_check_threshold +// 640 +// ``` +const PYTHONINTMAXSTRDIGITS: usize = 640;