mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 21:25:08 +00:00
[flake8-pyi
] Teach various rules that annotations might be stringized (#12951)
This commit is contained in:
parent
591a7a152c
commit
c0e2c13d0d
14 changed files with 184 additions and 51 deletions
|
@ -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,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": ...
|
||||
|
|
|
@ -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"):
|
||||
...
|
||||
|
|
|
@ -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"): ...
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 | ...
|
||||
|
|
||||
|
|
|
@ -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"): ...
|
||||
|
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue