From 2c7dcd30d6700e1f785eae696f95e3a24ddf23ef Mon Sep 17 00:00:00 2001 From: Julyx Date: Mon, 26 May 2025 00:50:43 +0300 Subject: [PATCH 1/5] [flake8-pyi] string typhints warning works same as for strait ones (PYI019) --- .../rules/custom_type_var_for_self.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index c3655eebdaf1e..662218c334b69 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -139,7 +139,20 @@ pub(crate) fn custom_type_var_instead_of_self( .chain(¶meters.args) .next()?; - let self_or_cls_annotation = self_or_cls_parameter.annotation()?; + let self_or_cls_annotation_unchecked = self_or_cls_parameter.annotation()?; + + let self_or_cls_annotation = match self_or_cls_annotation_unchecked { + ast::Expr::StringLiteral(_) => { + match checker.parse_type_annotation(self_or_cls_annotation_unchecked.as_string_literal_expr()?) { + Ok(parsed_annotation) => { parsed_annotation.expression() } + Err(_) => { return None } + } + } + ast::Expr::Subscript(_) | ast::Expr::Name(_) => { + self_or_cls_annotation_unchecked + } + _ => {return None} + }; let parent_class = current_scope.kind.as_class()?; // Skip any abstract/static/overloaded methods, @@ -193,7 +206,7 @@ pub(crate) fn custom_type_var_instead_of_self( function_def, custom_typevar, self_or_cls_parameter, - self_or_cls_annotation, + self_or_cls_annotation_unchecked, ) }); From 43811580a33dddc5ce0cdbcdf27c03f7e28d67a8 Mon Sep 17 00:00:00 2001 From: Julyx Date: Mon, 26 May 2025 07:50:31 +0300 Subject: [PATCH 2/5] [flake-pyi] snapshots update (PYI019) --- .../test/fixtures/flake8_pyi/PYI019_0.py | 31 +++++++ .../test/fixtures/flake8_pyi/PYI019_0.pyi | 33 +++++++ ...flake8_pyi__tests__PYI019_PYI019_0.py.snap | 93 +++++++++++++++++++ ...lake8_pyi__tests__PYI019_PYI019_0.pyi.snap | 93 +++++++++++++++++++ 4 files changed, 250 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.py index e502d8fc1f79d..0762e9f15c486 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.py @@ -181,3 +181,34 @@ def m[S](self: S) -> S: class MetaTestClass(type): def m(cls: MetaType) -> MetaType: return cls + + +from __future__ import annotations + +class BadClassWithStringTypeHints: + def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + + @classmethod + def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + + + @classmethod + def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + + + @classmethod + def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + + +class BadSubscriptReturnTypeWithStringTypeHints: + @classmethod + def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + + +class GoodClassWiStringTypeHints: + @classmethod + def good_cls_method_with_mixed_annotations(cls: "type[Self]", arg: str) -> Self: ... + @staticmethod + def good_static_method_with_string_annotations(arg: "_S") -> "_S": ... + @classmethod + def good_class_method_with_args_string_annotations(cls, arg1: "_S", arg2: "_S") -> "_S": ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.pyi index 1f2f17fc1b303..bffdb13dbb558 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.pyi @@ -172,3 +172,36 @@ MetaType = TypeVar("MetaType") class MetaTestClass(type): def m(cls: MetaType) -> MetaType: return cls + + +from __future__ import annotations + + +class BadClassWithStringTypeHints: + def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + + @classmethod + def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + + + @classmethod + def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + + + @classmethod + def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + + +class BadSubscriptReturnTypeWithStringTypeHints: + @classmethod + def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + + +class GoodClassWithStringTypeHints: + @classmethod + def good_cls_method_with_mixed_annotations(cls: "type[Self]", arg: str) -> Self: ... + @staticmethod + def good_static_method_with_string_annotations(arg: "_S") -> "_S": ... + @classmethod + def good_class_method_with_args_string_annotations(cls, arg1: "_S", arg2: "_S") -> "_S": ... + diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.py.snap index 1bbfbef8e2c36..4071c9595fbb8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.py.snap @@ -690,3 +690,96 @@ PYI019_0.py:173:10: PYI019 [*] Use `Self` instead of custom TypeVar `S` 174 174 | type S = int 175 175 | print(S) # not a reference to the type variable, so not touched by the autofix 176 176 | return 42 + +PYI019_0.py:189:52: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +188 | class BadClassWithStringTypeHints: +189 | def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 +190 | +191 | @classmethod + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +186 186 | from __future__ import annotations +187 187 | +188 188 | class BadClassWithStringTypeHints: +189 |- def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + 189 |+ def bad_instance_method_with_string_annotations(self, arg: str) -> "Self": ... # PYI019 +190 190 | +191 191 | @classmethod +192 192 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + +PYI019_0.py:192:49: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +191 | @classmethod +192 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +189 189 | def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 +190 190 | +191 191 | @classmethod +192 |- def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + 192 |+ def bad_class_method_with_string_annotations(cls) -> "Self": ... # PYI019 +193 193 | +194 194 | +195 195 | @classmethod + +PYI019_0.py:196:50: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +195 | @classmethod +196 | def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +193 193 | +194 194 | +195 195 | @classmethod +196 |- def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + 196 |+ def bad_class_method_with_mixed_annotations_1(cls) -> Self: ... # PYI019 +197 197 | +198 198 | +199 199 | @classmethod + +PYI019_0.py:200:50: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +199 | @classmethod +200 | def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +197 197 | +198 198 | +199 199 | @classmethod +200 |- def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + 200 |+ def bad_class_method_with_mixed_annotations_1(cls) -> "Self": ... # PYI019 +201 201 | +202 202 | +203 203 | class BadSubscriptReturnTypeWithStringTypeHints: + +PYI019_0.py:205:10: PYI019 [*] Use `Self` instead of custom TypeVar `S` + | +203 | class BadSubscriptReturnTypeWithStringTypeHints: +204 | @classmethod +205 | def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `S` with `Self` + +ℹ Safe fix +202 202 | +203 203 | class BadSubscriptReturnTypeWithStringTypeHints: +204 204 | @classmethod +205 |- def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + 205 |+ def m(cls) -> "type[Self]": ... # PYI019 +206 206 | +207 207 | +208 208 | class GoodClassWiStringTypeHints: diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.pyi.snap index 7c5b794bd5621..9b4647f3fe363 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.pyi.snap @@ -682,3 +682,96 @@ PYI019_0.pyi:167:10: PYI019 [*] Use `Self` instead of custom TypeVar `S` 168 168 | 169 169 | 170 170 | MetaType = TypeVar("MetaType") + +PYI019_0.pyi:181:52: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +180 | class BadClassWithStringTypeHints: +181 | def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 +182 | +183 | @classmethod + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +178 178 | +179 179 | +180 180 | class BadClassWithStringTypeHints: +181 |- def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + 181 |+ def bad_instance_method_with_string_annotations(self, arg: str) -> "Self": ... # PYI019 +182 182 | +183 183 | @classmethod +184 184 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + +PYI019_0.pyi:184:49: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +183 | @classmethod +184 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +181 181 | def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 +182 182 | +183 183 | @classmethod +184 |- def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + 184 |+ def bad_class_method_with_string_annotations(cls) -> "Self": ... # PYI019 +185 185 | +186 186 | +187 187 | @classmethod + +PYI019_0.pyi:188:50: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +187 | @classmethod +188 | def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +185 185 | +186 186 | +187 187 | @classmethod +188 |- def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + 188 |+ def bad_class_method_with_mixed_annotations_1(cls) -> Self: ... # PYI019 +189 189 | +190 190 | +191 191 | @classmethod + +PYI019_0.pyi:192:50: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +191 | @classmethod +192 | def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +189 189 | +190 190 | +191 191 | @classmethod +192 |- def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + 192 |+ def bad_class_method_with_mixed_annotations_1(cls) -> "Self": ... # PYI019 +193 193 | +194 194 | +195 195 | class BadSubscriptReturnTypeWithStringTypeHints: + +PYI019_0.pyi:197:10: PYI019 [*] Use `Self` instead of custom TypeVar `S` + | +195 | class BadSubscriptReturnTypeWithStringTypeHints: +196 | @classmethod +197 | def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `S` with `Self` + +ℹ Safe fix +194 194 | +195 195 | class BadSubscriptReturnTypeWithStringTypeHints: +196 196 | @classmethod +197 |- def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + 197 |+ def m(cls) -> "type[Self]": ... # PYI019 +198 198 | +199 199 | +200 200 | class GoodClassWithStringTypeHints: From 87c9e608a06ae11414db557c8f375d84e3a9054f Mon Sep 17 00:00:00 2001 From: Julyx Date: Mon, 26 May 2025 08:40:30 +0300 Subject: [PATCH 3/5] [flake-pyi] annotation normalization refactoring (PYI019) --- .../rules/flake8_pyi/rules/custom_type_var_for_self.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index 662218c334b69..82ae5b2aa8560 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -143,14 +143,10 @@ pub(crate) fn custom_type_var_instead_of_self( let self_or_cls_annotation = match self_or_cls_annotation_unchecked { ast::Expr::StringLiteral(_) => { - match checker.parse_type_annotation(self_or_cls_annotation_unchecked.as_string_literal_expr()?) { - Ok(parsed_annotation) => { parsed_annotation.expression() } - Err(_) => { return None } - } - } - ast::Expr::Subscript(_) | ast::Expr::Name(_) => { - self_or_cls_annotation_unchecked + let literal_expr = self_or_cls_annotation_unchecked.as_string_literal_expr()?; + checker.parse_type_annotation(literal_expr).ok()?.expression() } + ast::Expr::Subscript(_) | ast::Expr::Name(_) => self_or_cls_annotation_unchecked, _ => {return None} }; let parent_class = current_scope.kind.as_class()?; From 99e8ffea110cfe6c90314a2b3f194dc7a74c1151 Mon Sep 17 00:00:00 2001 From: Julyx Date: Wed, 28 May 2025 18:15:36 +0300 Subject: [PATCH 4/5] [flake-pyi] short circuits fix (PYI019) --- .../flake8_pyi/rules/custom_type_var_for_self.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index f102117b2d0e4..67f6c818620bd 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -147,11 +147,17 @@ pub(crate) fn custom_type_var_instead_of_self(checker: &Checker, binding: &Bindi }; let self_or_cls_annotation = match self_or_cls_annotation_unchecked { ast::Expr::StringLiteral(_) => { - let literal_expr = self_or_cls_annotation_unchecked.as_string_literal_expr()?; - checker.parse_type_annotation(literal_expr).ok()?.expression() + let Some(literal_expr) = self_or_cls_annotation_unchecked.as_string_literal_expr() + else { + return; + }; + let Ok(parsed_expr) = checker.parse_type_annotation(literal_expr) else { + return; + }; + parsed_expr.expression() } ast::Expr::Subscript(_) | ast::Expr::Name(_) => self_or_cls_annotation_unchecked, - _ => {return} + _ => return, }; let Some(parent_class) = current_scope.kind.as_class() else { return; From b2d8710650cd62a45543c1e39a351a3c79e41e3c Mon Sep 17 00:00:00 2001 From: Julyx Date: Sat, 14 Jun 2025 13:53:38 +0300 Subject: [PATCH 5/5] [flake-pyi] match simplification and extra filtration removed (PYI019) --- .../rules/flake8_pyi/rules/custom_type_var_for_self.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index 67f6c818620bd..91a8a9f140080 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -146,18 +146,13 @@ pub(crate) fn custom_type_var_instead_of_self(checker: &Checker, binding: &Bindi return; }; let self_or_cls_annotation = match self_or_cls_annotation_unchecked { - ast::Expr::StringLiteral(_) => { - let Some(literal_expr) = self_or_cls_annotation_unchecked.as_string_literal_expr() - else { - return; - }; + ast::Expr::StringLiteral(literal_expr) => { let Ok(parsed_expr) = checker.parse_type_annotation(literal_expr) else { return; }; parsed_expr.expression() } - ast::Expr::Subscript(_) | ast::Expr::Name(_) => self_or_cls_annotation_unchecked, - _ => return, + _ => self_or_cls_annotation_unchecked, }; let Some(parent_class) = current_scope.kind.as_class() else { return;