From c0e2c13d0d79bcadd936b68db9c33a4775f8573a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Sep 2024 14:40:06 +0100 Subject: [PATCH] [`flake8-pyi`] Teach various rules that annotations might be stringized (#12951) --- .../test/fixtures/flake8_pyi/PYI032.py | 8 ++- .../test/fixtures/flake8_pyi/PYI032.pyi | 7 ++- .../test/fixtures/flake8_pyi/PYI034.py | 4 ++ .../test/fixtures/flake8_pyi/PYI034.pyi | 3 ++ .../test/fixtures/flake8_pyi/PYI050.py | 8 +++ .../test/fixtures/flake8_pyi/PYI050.pyi | 2 + crates/ruff_linter/src/checkers/ast/mod.rs | 20 ++++++++ .../flake8_pyi/rules/any_eq_ne_annotation.rs | 40 ++++++++------- .../rules/no_return_argument_annotation.rs | 17 +++++-- .../flake8_pyi/rules/non_self_return_type.rs | 8 +-- ...__flake8_pyi__tests__PYI032_PYI032.py.snap | 51 +++++++++++++++---- ..._flake8_pyi__tests__PYI032_PYI032.pyi.snap | 51 +++++++++++++++---- ...__flake8_pyi__tests__PYI050_PYI050.py.snap | 7 ++- ..._flake8_pyi__tests__PYI050_PYI050.pyi.snap | 9 +++- 14 files changed, 184 insertions(+), 51 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI032.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI032.py index 82cb899e3b323..d1e70d27c17fe 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI032.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI032.py @@ -3,8 +3,8 @@ class Bad: - def __eq__(self, other: Any) -> bool: ... # Y032 - def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 + def __eq__(self, other: Any) -> bool: ... # PYI032 + def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 class Good: @@ -22,3 +22,7 @@ class Unannotated: def __eq__(self) -> Any: ... def __ne__(self) -> bool: ... + +class BadStringized: + def __eq__(self, other: "Any") -> bool: ... # PYI032 + def __ne__(self, other: "Any") -> bool: ... # PYI032 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI032.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI032.pyi index 82cb899e3b323..2fe211b079000 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI032.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI032.pyi @@ -3,8 +3,8 @@ import typing class Bad: - def __eq__(self, other: Any) -> bool: ... # Y032 - def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 + def __eq__(self, other: Any) -> bool: ... # PYI032 + def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 class Good: @@ -22,3 +22,6 @@ class Unannotated: def __eq__(self) -> Any: ... def __ne__(self) -> bool: ... +class BadStringized: + def __eq__(self, other: "Any") -> bool: ... # PYI032 + def __ne__(self, other: "Any") -> bool: ... # PYI032 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py index 90257717e8c14..179c9db90c4c5 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py @@ -317,3 +317,7 @@ def __ne__(self, other: Any) -> bool: def __imul__(self, other: Any) -> list[str]: ... + +class UsesStringizedAnnotations: + def __iadd__(self, other: "UsesStringizedAnnotations") -> "typing.Self": + return self diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi index 8188312a03833..ebecadfbd50cc 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi @@ -212,3 +212,6 @@ def __str__(self) -> str: ... def __eq__(self, other: Any) -> bool: ... def __ne__(self, other: Any) -> bool: ... def __imul__(self, other: Any) -> list[str]: ... + +class UsesStringizedAnnotations: + def __iadd__(self, other: "UsesStringizedAnnotations") -> "typing.Self": ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.py index 042fe887ec36d..9bbffdeadd3ca 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.py @@ -30,3 +30,11 @@ def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): def foo_never(arg: Never): ... + + +def stringized(arg: "NoReturn"): + ... + + +def stringized_good(arg: "Never"): + ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.pyi index 583c96e71b59b..4dc3fadb86bd0 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI050.pyi @@ -21,3 +21,5 @@ def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI0 def foo_args_never(*args: Never): ... def foo_kwargs_never(**kwargs: Never): ... def foo_args_kwargs_never(*args: Never, **kwargs: Never): ... +def stringized(arg: "NoReturn"): ... +def stringized_good(arg: "Never"): ... diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 4e583bb12750a..6669132b33131 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -424,6 +424,26 @@ impl<'a> Checker<'a> { self.parsed_annotations_cache .lookup_or_parse(annotation, self.locator.contents()) } + + /// Apply a test to an annotation expression, + /// abstracting over the fact that the annotation expression might be "stringized". + /// + /// A stringized annotation is one enclosed in string quotes: + /// `foo: "typing.Any"` means the same thing to a type checker as `foo: typing.Any`. + pub(crate) fn match_maybe_stringized_annotation( + &self, + expr: &ast::Expr, + match_fn: impl FnOnce(&ast::Expr) -> bool, + ) -> bool { + if let ast::Expr::StringLiteral(string_annotation) = expr { + let Some(parsed_annotation) = self.parse_type_annotation(string_annotation) else { + return false; + }; + match_fn(parsed_annotation.expression()) + } else { + match_fn(expr) + } + } } impl<'a> Visitor<'a> for Checker<'a> { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs index df6244fb2c988..01a2d1447e3f1 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs @@ -78,23 +78,27 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, parameters return; } - if semantic.match_typing_expr(annotation, "Any") { - let mut diagnostic = Diagnostic::new( - AnyEqNeAnnotation { - method_name: name.to_string(), - }, - annotation.range(), - ); - // Ex) `def __eq__(self, obj: Any): ...` - diagnostic.try_set_fix(|| { - let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( - "object", - annotation.start(), - semantic, - )?; - let binding_edit = Edit::range_replacement(binding, annotation.range()); - Ok(Fix::safe_edits(binding_edit, import_edit)) - }); - checker.diagnostics.push(diagnostic); + if !checker.match_maybe_stringized_annotation(annotation, |expr| { + semantic.match_typing_expr(expr, "Any") + }) { + return; } + + let mut diagnostic = Diagnostic::new( + AnyEqNeAnnotation { + method_name: name.to_string(), + }, + annotation.range(), + ); + // Ex) `def __eq__(self, obj: Any): ...` + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( + "object", + annotation.start(), + semantic, + )?; + let binding_edit = Edit::range_replacement(binding, annotation.range()); + Ok(Fix::safe_edits(binding_edit, import_edit)) + }); + checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index f7decb5e5384f..e07f868407b23 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -2,7 +2,7 @@ use std::fmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::{AnyParameterRef, Parameters}; +use ruff_python_ast as ast; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -54,14 +54,17 @@ impl Violation for NoReturnArgumentAnnotationInStub { } /// PYI050 -pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &Parameters) { +pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &ast::Parameters) { // Ex) def func(arg: NoReturn): ... // Ex) def func(arg: NoReturn, /): ... // Ex) def func(*, arg: NoReturn): ... // Ex) def func(*args: NoReturn): ... // Ex) def func(**kwargs: NoReturn): ... - for annotation in parameters.iter().filter_map(AnyParameterRef::annotation) { - if checker.semantic().match_typing_expr(annotation, "NoReturn") { + for annotation in parameters + .iter() + .filter_map(ast::AnyParameterRef::annotation) + { + if is_no_return(annotation, checker) { checker.diagnostics.push(Diagnostic::new( NoReturnArgumentAnnotationInStub { module: if checker.settings.target_version >= Py311 { @@ -76,6 +79,12 @@ pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: & } } +fn is_no_return(expr: &ast::Expr, checker: &Checker) -> bool { + checker.match_maybe_stringized_annotation(expr, |expr| { + checker.semantic().match_typing_expr(expr, "NoReturn") + }) +} + #[derive(Debug, PartialEq, Eq)] enum TypingModule { Typing, 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 eaf028ffa761e..706c377b9dec0 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 @@ -149,7 +149,7 @@ pub(crate) fn non_self_return_type( // In-place methods that are expected to return `Self`. if is_inplace_bin_op(name) { - if !is_self(returns, semantic) { + if !is_self(returns, checker) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), @@ -235,8 +235,10 @@ fn is_name(expr: &Expr, name: &str) -> bool { } /// Return `true` if the given expression resolves to `typing.Self`. -fn is_self(expr: &Expr, semantic: &SemanticModel) -> bool { - semantic.match_typing_expr(expr, "Self") +fn is_self(expr: &Expr, checker: &Checker) -> bool { + checker.match_maybe_stringized_annotation(expr, |expr| { + checker.semantic().match_typing_expr(expr, "Self") + }) } /// Return `true` if the given class extends `collections.abc.Iterator`. diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.py.snap index 7415a2ab741be..b6b3713235846 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.py.snap @@ -4,9 +4,9 @@ source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs PYI032.py:6:29: PYI032 [*] Prefer `object` to `Any` for the second parameter to `__eq__` | 5 | class Bad: -6 | def __eq__(self, other: Any) -> bool: ... # Y032 +6 | def __eq__(self, other: Any) -> bool: ... # PYI032 | ^^^ PYI032 -7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 +7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 | = help: Replace with `object` @@ -14,17 +14,17 @@ PYI032.py:6:29: PYI032 [*] Prefer `object` to `Any` for the second parameter to 3 3 | 4 4 | 5 5 | class Bad: -6 |- def __eq__(self, other: Any) -> bool: ... # Y032 - 6 |+ def __eq__(self, other: object) -> bool: ... # Y032 -7 7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 +6 |- def __eq__(self, other: Any) -> bool: ... # PYI032 + 6 |+ def __eq__(self, other: object) -> bool: ... # PYI032 +7 7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 8 8 | 9 9 | PYI032.py:7:29: PYI032 [*] Prefer `object` to `Any` for the second parameter to `__ne__` | 5 | class Bad: -6 | def __eq__(self, other: Any) -> bool: ... # Y032 -7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 +6 | def __eq__(self, other: Any) -> bool: ... # PYI032 +7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 | ^^^^^^^^^^ PYI032 | = help: Replace with `object` @@ -32,11 +32,42 @@ PYI032.py:7:29: PYI032 [*] Prefer `object` to `Any` for the second parameter to ℹ Safe fix 4 4 | 5 5 | class Bad: -6 6 | def __eq__(self, other: Any) -> bool: ... # Y032 -7 |- def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 - 7 |+ def __ne__(self, other: object) -> typing.Any: ... # Y032 +6 6 | def __eq__(self, other: Any) -> bool: ... # PYI032 +7 |- def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 + 7 |+ def __ne__(self, other: object) -> typing.Any: ... # PYI032 8 8 | 9 9 | 10 10 | class Good: +PYI032.py:27:28: PYI032 [*] Prefer `object` to `Any` for the second parameter to `__eq__` + | +26 | class BadStringized: +27 | def __eq__(self, other: "Any") -> bool: ... # PYI032 + | ^^^^^ PYI032 +28 | def __ne__(self, other: "Any") -> bool: ... # PYI032 + | + = help: Replace with `object` +ℹ Safe fix +24 24 | +25 25 | +26 26 | class BadStringized: +27 |- def __eq__(self, other: "Any") -> bool: ... # PYI032 + 27 |+ def __eq__(self, other: object) -> bool: ... # PYI032 +28 28 | def __ne__(self, other: "Any") -> bool: ... # PYI032 + +PYI032.py:28:28: PYI032 [*] Prefer `object` to `Any` for the second parameter to `__ne__` + | +26 | class BadStringized: +27 | def __eq__(self, other: "Any") -> bool: ... # PYI032 +28 | def __ne__(self, other: "Any") -> bool: ... # PYI032 + | ^^^^^ PYI032 + | + = help: Replace with `object` + +ℹ Safe fix +25 25 | +26 26 | class BadStringized: +27 27 | def __eq__(self, other: "Any") -> bool: ... # PYI032 +28 |- def __ne__(self, other: "Any") -> bool: ... # PYI032 + 28 |+ def __ne__(self, other: object) -> bool: ... # PYI032 diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.pyi.snap index 0017fe8889720..76cd8c068fa5d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.pyi.snap @@ -4,9 +4,9 @@ source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs PYI032.pyi:6:29: PYI032 [*] Prefer `object` to `Any` for the second parameter to `__eq__` | 5 | class Bad: -6 | def __eq__(self, other: Any) -> bool: ... # Y032 +6 | def __eq__(self, other: Any) -> bool: ... # PYI032 | ^^^ PYI032 -7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 +7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 | = help: Replace with `object` @@ -14,17 +14,17 @@ PYI032.pyi:6:29: PYI032 [*] Prefer `object` to `Any` for the second parameter to 3 3 | 4 4 | 5 5 | class Bad: -6 |- def __eq__(self, other: Any) -> bool: ... # Y032 - 6 |+ def __eq__(self, other: object) -> bool: ... # Y032 -7 7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 +6 |- def __eq__(self, other: Any) -> bool: ... # PYI032 + 6 |+ def __eq__(self, other: object) -> bool: ... # PYI032 +7 7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 8 8 | 9 9 | PYI032.pyi:7:29: PYI032 [*] Prefer `object` to `Any` for the second parameter to `__ne__` | 5 | class Bad: -6 | def __eq__(self, other: Any) -> bool: ... # Y032 -7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 +6 | def __eq__(self, other: Any) -> bool: ... # PYI032 +7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 | ^^^^^^^^^^ PYI032 | = help: Replace with `object` @@ -32,11 +32,42 @@ PYI032.pyi:7:29: PYI032 [*] Prefer `object` to `Any` for the second parameter to ℹ Safe fix 4 4 | 5 5 | class Bad: -6 6 | def __eq__(self, other: Any) -> bool: ... # Y032 -7 |- def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032 - 7 |+ def __ne__(self, other: object) -> typing.Any: ... # Y032 +6 6 | def __eq__(self, other: Any) -> bool: ... # PYI032 +7 |- def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 + 7 |+ def __ne__(self, other: object) -> typing.Any: ... # PYI032 8 8 | 9 9 | 10 10 | class Good: +PYI032.pyi:26:28: PYI032 [*] Prefer `object` to `Any` for the second parameter to `__eq__` + | +25 | class BadStringized: +26 | def __eq__(self, other: "Any") -> bool: ... # PYI032 + | ^^^^^ PYI032 +27 | def __ne__(self, other: "Any") -> bool: ... # PYI032 + | + = help: Replace with `object` +ℹ Safe fix +23 23 | def __ne__(self) -> bool: ... +24 24 | +25 25 | class BadStringized: +26 |- def __eq__(self, other: "Any") -> bool: ... # PYI032 + 26 |+ def __eq__(self, other: object) -> bool: ... # PYI032 +27 27 | def __ne__(self, other: "Any") -> bool: ... # PYI032 + +PYI032.pyi:27:28: PYI032 [*] Prefer `object` to `Any` for the second parameter to `__ne__` + | +25 | class BadStringized: +26 | def __eq__(self, other: "Any") -> bool: ... # PYI032 +27 | def __ne__(self, other: "Any") -> bool: ... # PYI032 + | ^^^^^ PYI032 + | + = help: Replace with `object` + +ℹ Safe fix +24 24 | +25 25 | class BadStringized: +26 26 | def __eq__(self, other: "Any") -> bool: ... # PYI032 +27 |- def __ne__(self, other: "Any") -> bool: ... # PYI032 + 27 |+ def __ne__(self, other: object) -> bool: ... # PYI032 diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap index 1f70291a11d98..10e43df19dda8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap @@ -31,4 +31,9 @@ PYI050.py:27:47: PYI050 Prefer `typing.Never` over `NoReturn` for argument annot 28 | ... | - +PYI050.py:35:21: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +35 | def stringized(arg: "NoReturn"): + | ^^^^^^^^^^ PYI050 +36 | ... + | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap index 9e6e3ea29006c..6b8acaec8375f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap @@ -101,4 +101,11 @@ PYI050.pyi:20:37: PYI050 Prefer `typing.Never` over `NoReturn` for argument anno 22 | def foo_kwargs_never(**kwargs: Never): ... | - +PYI050.pyi:24:21: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +22 | def foo_kwargs_never(**kwargs: Never): ... +23 | def foo_args_kwargs_never(*args: Never, **kwargs: Never): ... +24 | def stringized(arg: "NoReturn"): ... + | ^^^^^^^^^^ PYI050 +25 | def stringized_good(arg: "Never"): ... + |