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/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/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/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 e2b0efc8164fe..fc7ffc47e3809 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, @@ -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<'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 @@ -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, .. })) - if class.is_known(db, KnownClass::Bool) => + (Type::BooleanLiteral(_), Type::Instance(InstanceType { class })) + if matches!(class.known(db), Some(KnownClass::Bool | KnownClass::Int)) => { 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, } @@ -627,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() @@ -654,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, .. }) + 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) + ) || ( + self_class.is_known(db, KnownClass::NoDefaultType) && + target_class.is_known(db, KnownClass::NoDefaultType) ) - if self_class.is_known(db, KnownClass::NoneType) && - target_class.is_known(db, KnownClass::NoneType)) + ) } /// Return true if this type and `other` have no common elements. @@ -753,88 +762,87 @@ 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) ), (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 @@ -902,10 +910,14 @@ impl<'db> Type<'db> { Type::BooleanLiteral(_) | Type::FunctionLiteral(..) | Type::ClassLiteral(..) - | Type::ModuleLiteral(..) => true, - Type::Instance(InstanceType { class, .. }) => { - // TODO some more instance types can be singleton types (EllipsisType, NotImplementedType) - matches!(class.known(db), Some(KnownClass::NoneType)) + | Type::ModuleLiteral(..) + | Type::KnownInstance(..) => true, + Type::Instance(InstanceType { class }) => { + 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 @@ -944,7 +956,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,8 +969,8 @@ impl<'db> Type<'db> { .iter() .all(|elem| elem.is_single_valued(db)), - Type::Instance(InstanceType { class, .. }) => match class.known(db) { - Some(KnownClass::NoneType) => true, + Type::Instance(InstanceType { class }) => match class.known(db) { + Some(KnownClass::NoneType | KnownClass::NoDefaultType) => true, Some( KnownClass::Bool | KnownClass::Object @@ -974,7 +987,8 @@ impl<'db> Type<'db> { | KnownClass::GenericAlias | KnownClass::ModuleType | KnownClass::FunctionType - | KnownClass::SpecialForm, + | KnownClass::SpecialForm + | KnownClass::TypeVar, ) => false, None => false, }, @@ -1045,6 +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.member(db, name), Type::Instance(_) => { // TODO MRO? get_own_instance_member, get_instance_member Type::Todo.into() @@ -1139,7 +1154,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 +1164,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 +1220,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 +1371,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 +1386,7 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::FunctionLiteral(_) | Type::Instance(_) + | Type::KnownInstance(_) | Type::ModuleLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) @@ -1375,10 +1396,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 +1407,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 +1449,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(db))) + } // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), } @@ -1450,6 +1471,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(db))) + } // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), } @@ -1498,7 +1522,10 @@ pub enum KnownClass { FunctionType, // Typeshed NoneType, // Part of `types` for Python >= 3.10 + // Typing SpecialForm, + TypeVar, + NoDefaultType, } impl<'db> KnownClass { @@ -1521,6 +1548,8 @@ impl<'db> KnownClass { Self::FunctionType => "FunctionType", Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", + Self::TypeVar => "TypeVar", + Self::NoDefaultType => "_NoDefaultType", } } @@ -1545,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(), + } + } + + const 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, } } @@ -1581,6 +1636,7 @@ impl<'db> KnownClass { "ModuleType" => Some(Self::ModuleType), "FunctionType" => Some(Self::FunctionType), "_SpecialForm" => Some(Self::SpecialForm), + "_NoDefaultType" => Some(Self::NoDefaultType), _ => None, } } @@ -1605,48 +1661,160 @@ 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") } } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum KnownInstance { +/// Enumeration of specific runtime that are special enough to be considered their own type. +#[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 KnownInstance { - pub const fn as_str(&self) -> &'static str { +impl<'db> KnownInstanceType<'db> { + pub const fn as_str(self) -> &'static str { match self { - KnownInstance::Literal => "Literal", + KnownInstanceType::Literal => "Literal", + KnownInstanceType::TypeVar(_) => "TypeVar", } } - 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, + Self::TypeVar(_) => Truthiness::AlwaysTrue, + } } - fn from_name(name: &str) -> Option { - match name { - "Literal" => Some(Self::Literal), - _ => None, + /// Return the repr of the symbol at runtime + pub fn repr(self, db: &'db dyn Db) -> &'db str { + match self { + Self::Literal => "typing.Literal", + Self::TypeVar(typevar) => typevar.name(db), } } - 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, + Self::TypeVar(_) => KnownClass::TypeVar, + } + } + + /// 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, } } + + 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)] @@ -1993,7 +2161,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` @@ -2181,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); @@ -2280,86 +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); - - // 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 - } else if let Some(base_class) = base_classes.next() { - safe_recurse(base_class.class)? - } else { - KnownClass::Type.to_class(db) - }; + 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); + } - let Type::ClassLiteral(mut candidate) = metaclass 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 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) + }; - // 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; - }; - if metaclass.class.is_subclass_of(db, candidate.class) { - candidate = metaclass; - continue; - } - if candidate.class.is_subclass_of(db, metaclass.class) { - continue; - } - return Err(MetaclassError { - kind: MetaclassErrorKind::Conflict { - metaclass1: candidate.class, - metaclass2: metaclass.class, - }, - }); + 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); + }; - Ok(Type::ClassLiteral(candidate)) + // 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, + }; + 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, + }, + 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`. @@ -2406,42 +2577,50 @@ impl<'db> Class<'db> { let scope = self.body_scope(db); symbol(db, scope, name) } -} -/// 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(), + /// 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 } - } - - fn len(&self) -> usize { - self.visited.len() - } - fn truncate(&mut self, len: usize) { - self.visited.truncate(len); + self.fully_static_explicit_bases(db) + .any(|base_class| is_cyclically_defined_recursive(db, base_class, &mut IndexSet::new())) } +} - fn insert(&mut self, value: T) -> bool { - if value == self.initial { - return false; - } - self.visited.insert(value) - } +/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct MetaclassCandidate<'db> { + metaclass: Class<'db>, + explicit_metaclass_of: Class<'db>, } /// 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>, } @@ -2463,7 +2642,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>, } @@ -2475,37 +2654,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)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] 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) @@ -2537,15 +2691,17 @@ 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, - /// 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] @@ -2703,8 +2859,11 @@ mod tests { BytesLiteral(&'static str), // BuiltinInstance("str") corresponds to an instance of the builtin `str` class BuiltinInstance(&'static str), + TypingInstance(&'static str), + 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), @@ -2724,7 +2883,10 @@ 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::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))) } @@ -2761,6 +2923,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) { @@ -2772,6 +2942,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))); @@ -2785,6 +2963,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"))] @@ -2813,6 +2992,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::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))); @@ -2983,6 +3164,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); @@ -3054,6 +3236,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/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 2ff1f9fad1547..d061971dd37f4 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 @@ -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(); diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 9e0b5dfeede35..5bcafd902e6be 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -66,11 +66,16 @@ 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") } + 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"), @@ -82,10 +87,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 ed26412c87a01..7f6fbdd6bd496 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -58,8 +58,9 @@ 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, - TupleType, Type, TypeArrayDisplay, UnionBuilder, UnionType, + KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, StringLiteralType, + Symbol, Truthiness, TupleType, Type, TypeArrayDisplay, TypeVarBoundOrConstraints, + TypeVarInstance, UnionBuilder, UnionType, }; use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; @@ -456,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 @@ -470,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) => { @@ -483,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 { @@ -517,24 +531,53 @@ 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 { - 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), - ), - ), - MetaclassErrorKind::CyclicDefinition => { - // Cyclic class definition diagnostic will already have been emitted above - // in MRO calculation. + 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), + ) + ); + } } } } @@ -610,6 +653,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); + } } } @@ -634,7 +686,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 +1349,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 }) }) }), ), @@ -1318,6 +1372,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: _, @@ -1503,14 +1633,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 +1687,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()) { @@ -1977,13 +2109,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!( @@ -2717,7 +2842,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__", @@ -3204,18 +3329,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 { @@ -3834,7 +3973,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) @@ -3862,32 +4001,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), } } } @@ -3921,6 +4037,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 @@ -4095,6 +4218,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`, @@ -4180,10 +4310,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 @@ -4193,11 +4322,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 { @@ -4213,6 +4342,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Unknown } }, + KnownInstanceType::TypeVar(_) => Type::Todo, } } @@ -4224,13 +4354,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..8ff2279e36b57 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -1,11 +1,10 @@ use std::collections::VecDeque; use std::ops::Deref; -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. @@ -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 @@ -379,6 +370,10 @@ impl<'db> ClassBase<'db> { | Type::SliceLiteral(_) | Type::ModuleLiteral(_) | Type::SubclassOf(_) => None, + Type::KnownInstance(known_instance) => match known_instance { + KnownInstanceType::Literal => None, + KnownInstanceType::TypeVar(_) => None, + }, } } @@ -457,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())) -} 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 => { 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: + ... 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_linter/resources/test/fixtures/pylint/await_outside_async.py b/crates/ruff_linter/resources/test/fixtures/pylint/await_outside_async.py index ea91f1bf91f06..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 @@ -32,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/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/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/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/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/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_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"] 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..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 @@ -32,7 +31,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; @@ -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/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/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_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; 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/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; 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 diff --git a/crates/ruff_linter/src/rules/ruff/typing.rs b/crates/ruff_linter/src/rules/ruff/typing.rs index e7cab12ed8ea8..f7ed900719e0e 100644 --- a/crates/ruff_linter/src/rules/ruff/typing.rs +++ b/crates/ruff_linter/src/rules/ruff/typing.rs @@ -286,6 +286,8 @@ pub(crate) fn type_hint_resolves_to_any( match TypingTarget::try_from_expr(annotation, checker, minor_version) { // Short circuit on top level `Any` None | Some(TypingTarget::Any) => true, + // `Optional` is `Optional[Any]` which is `Any | None`. + Some(TypingTarget::Optional(None)) => true, // Top-level `Annotated` node should check if the inner type resolves // to `Any`. Some(TypingTarget::Annotated(expr)) => { 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` 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 "]