mirror of https://github.com/astral-sh/ruff
[`flake8-pyi`] Implement PYI036 (#5668)
## Summary Implements PYI036 from `flake8-pyi`. See [original code](https://github.com/PyCQA/flake8-pyi/blob/main/pyi.py#L1585) ## Test Plan - Updated snapshots - Checked against manual runs of flake8 ref: #848
This commit is contained in:
parent
2b03bd18f4
commit
19f475ae1f
|
|
@ -0,0 +1,75 @@
|
|||
import builtins
|
||||
import types
|
||||
import typing
|
||||
from collections.abc import Awaitable
|
||||
from types import TracebackType
|
||||
from typing import Any, Type
|
||||
|
||||
import _typeshed
|
||||
import typing_extensions
|
||||
from _typeshed import Unused
|
||||
|
||||
class GoodOne:
|
||||
def __exit__(self, *args: object) -> None: ...
|
||||
async def __aexit__(self, *args) -> str: ...
|
||||
|
||||
class GoodTwo:
|
||||
def __exit__(self, typ: type[builtins.BaseException] | None, *args: builtins.object) -> bool | None: ...
|
||||
async def __aexit__(self, /, typ: Type[BaseException] | None, *args: object, **kwargs) -> bool: ...
|
||||
|
||||
class GoodThree:
|
||||
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ...
|
||||
async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ...
|
||||
|
||||
class GoodFour:
|
||||
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
|
||||
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None, *args: list[None]) -> None: ...
|
||||
|
||||
class GoodFive:
|
||||
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: int, **kwargs: str) -> None: ...
|
||||
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> Awaitable[None]: ...
|
||||
|
||||
class GoodSix:
|
||||
def __exit__(self, typ: object, exc: builtins.object, tb: object) -> None: ...
|
||||
async def __aexit__(self, typ: object, exc: object, tb: builtins.object) -> None: ...
|
||||
|
||||
class GoodSeven:
|
||||
def __exit__(self, *args: Unused) -> bool: ...
|
||||
async def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ...
|
||||
|
||||
class GoodEight:
|
||||
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ...
|
||||
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
|
||||
|
||||
class GoodNine:
|
||||
def __exit__(self, __typ: typing.Union[typing.Type[BaseException] , None], exc: typing.Union[BaseException , None], *args: _typeshed.Unused) -> bool: ...
|
||||
async def __aexit__(self, typ: typing.Union[typing.Type[BaseException], None], exc: typing.Union[BaseException , None], tb: typing.Union[TracebackType , None], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
|
||||
|
||||
class GoodTen:
|
||||
def __exit__(self, __typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], *args: _typeshed.Unused) -> bool: ...
|
||||
async def __aexit__(self, typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], tb: typing.Optional[TracebackType], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
|
||||
|
||||
|
||||
class BadOne:
|
||||
def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation
|
||||
async def __aexit__(self) -> None: ... # PYI036: Missing args
|
||||
|
||||
class BadTwo:
|
||||
def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default
|
||||
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ...# PYI036: Extra arg must have default
|
||||
|
||||
class BadThree:
|
||||
def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation
|
||||
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation
|
||||
|
||||
class BadFour:
|
||||
def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation
|
||||
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation
|
||||
|
||||
class BadFive:
|
||||
def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
|
||||
async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
|
||||
|
||||
class BadSix:
|
||||
def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
|
||||
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import builtins
|
||||
import types
|
||||
import typing
|
||||
from collections.abc import Awaitable
|
||||
from types import TracebackType
|
||||
from typing import Any, Type
|
||||
|
||||
import _typeshed
|
||||
import typing_extensions
|
||||
from _typeshed import Unused
|
||||
|
||||
class GoodOne:
|
||||
def __exit__(self, *args: object) -> None: ...
|
||||
async def __aexit__(self, *args) -> str: ...
|
||||
|
||||
class GoodTwo:
|
||||
def __exit__(self, typ: type[builtins.BaseException] | None, *args: builtins.object) -> bool | None: ...
|
||||
async def __aexit__(self, /, typ: Type[BaseException] | None, *args: object, **kwargs) -> bool: ...
|
||||
|
||||
class GoodThree:
|
||||
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ...
|
||||
async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ...
|
||||
|
||||
class GoodFour:
|
||||
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
|
||||
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None, *args: list[None]) -> None: ...
|
||||
|
||||
class GoodFive:
|
||||
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: int, **kwargs: str) -> None: ...
|
||||
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> Awaitable[None]: ...
|
||||
|
||||
class GoodSix:
|
||||
def __exit__(self, typ: object, exc: builtins.object, tb: object) -> None: ...
|
||||
async def __aexit__(self, typ: object, exc: object, tb: builtins.object) -> None: ...
|
||||
|
||||
class GoodSeven:
|
||||
def __exit__(self, *args: Unused) -> bool: ...
|
||||
async def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ...
|
||||
|
||||
class GoodEight:
|
||||
def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ...
|
||||
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
|
||||
|
||||
class GoodNine:
|
||||
def __exit__(self, __typ: typing.Union[typing.Type[BaseException] , None], exc: typing.Union[BaseException , None], *args: _typeshed.Unused) -> bool: ...
|
||||
async def __aexit__(self, typ: typing.Union[typing.Type[BaseException], None], exc: typing.Union[BaseException , None], tb: typing.Union[TracebackType , None], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
|
||||
|
||||
class GoodTen:
|
||||
def __exit__(self, __typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], *args: _typeshed.Unused) -> bool: ...
|
||||
async def __aexit__(self, typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], tb: typing.Optional[TracebackType], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ...
|
||||
|
||||
|
||||
class BadOne:
|
||||
def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation
|
||||
async def __aexit__(self) -> None: ... # PYI036: Missing args
|
||||
|
||||
class BadTwo:
|
||||
def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default
|
||||
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default
|
||||
|
||||
class BadThree:
|
||||
def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation
|
||||
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation
|
||||
|
||||
class BadFour:
|
||||
def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation
|
||||
async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation
|
||||
|
||||
class BadFive:
|
||||
def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
|
||||
async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
|
||||
|
||||
class BadSix:
|
||||
def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
|
||||
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default
|
||||
|
|
@ -435,6 +435,14 @@ where
|
|||
if self.enabled(Rule::NoReturnArgumentAnnotationInStub) {
|
||||
flake8_pyi::rules::no_return_argument_annotation(self, args);
|
||||
}
|
||||
if self.enabled(Rule::BadExitAnnotation) {
|
||||
flake8_pyi::rules::bad_exit_annotation(
|
||||
self,
|
||||
stmt.is_async_function_def_stmt(),
|
||||
name,
|
||||
args,
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.enabled(Rule::DunderFunctionName) {
|
||||
if let Some(diagnostic) = pep8_naming::rules::dunder_function_name(
|
||||
|
|
|
|||
|
|
@ -638,6 +638,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8Pyi, "033") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeCommentInStub),
|
||||
(Flake8Pyi, "034") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NonSelfReturnType),
|
||||
(Flake8Pyi, "035") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnassignedSpecialVariableInStub),
|
||||
(Flake8Pyi, "036") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadExitAnnotation),
|
||||
(Flake8Pyi, "042") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::SnakeCaseTypeAlias),
|
||||
(Flake8Pyi, "043") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TSuffixedTypeAlias),
|
||||
(Flake8Pyi, "044") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::FutureAnnotationsInStub),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ mod tests {
|
|||
#[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.pyi"))]
|
||||
#[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.py"))]
|
||||
#[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.pyi"))]
|
||||
#[test_case(Rule::BadExitAnnotation, Path::new("PYI036.py"))]
|
||||
#[test_case(Rule::BadExitAnnotation, Path::new("PYI036.pyi"))]
|
||||
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.py"))]
|
||||
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,351 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use rustpython_parser::ast::{
|
||||
ArgWithDefault, Arguments, Expr, ExprBinOp, ExprSubscript, ExprTuple, Identifier, Operator,
|
||||
Ranged,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_none;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for incorrect function signatures on `__exit__` and `__aexit__`
|
||||
/// methods.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Improperly-annotated `__exit__` and `__aexit__` methods can cause
|
||||
/// unexpected behavior when interacting with type checkers.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __exit__(self, typ, exc, tb, extra_arg) -> None:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __exit__(
|
||||
/// self,
|
||||
/// typ: type[BaseException] | None,
|
||||
/// exc: BaseException | None,
|
||||
/// tb: TracebackType | None,
|
||||
/// extra_arg: int = 0,
|
||||
/// ) -> None:
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BadExitAnnotation {
|
||||
func_kind: FuncKind,
|
||||
error_kind: ErrorKind,
|
||||
}
|
||||
|
||||
impl Violation for BadExitAnnotation {
|
||||
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let method_name = self.func_kind.to_string();
|
||||
match self.error_kind {
|
||||
ErrorKind::StarArgsNotAnnotated => format!("Star-args in `{method_name}` should be annotated with `object`"),
|
||||
ErrorKind::MissingArgs => format!("If there are no star-args, `{method_name}` should have at least 3 non-keyword-only args (excluding `self`)"),
|
||||
ErrorKind::ArgsAfterFirstFourMustHaveDefault => format!("All arguments after the first four in `{method_name}` must have a default value"),
|
||||
ErrorKind::AllKwargsMustHaveDefault => format!("All keyword-only arguments in `{method_name}` must have a default value"),
|
||||
ErrorKind::FirstArgBadAnnotation => format!("The first argument in `{method_name}` should be annotated with `object` or `type[BaseException] | None`"),
|
||||
ErrorKind::SecondArgBadAnnotation => format!("The second argument in `{method_name}` should be annotated with `object` or `BaseException | None`"),
|
||||
ErrorKind::ThirdArgBadAnnotation => format!("The third argument in `{method_name}` should be annotated with `object` or `types.TracebackType | None`"),
|
||||
}
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
if matches!(self.error_kind, ErrorKind::StarArgsNotAnnotated) {
|
||||
Some("Annotate star-args with `object`".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
enum FuncKind {
|
||||
Sync,
|
||||
Async,
|
||||
}
|
||||
|
||||
impl Display for FuncKind {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FuncKind::Sync => write!(f, "__exit__"),
|
||||
FuncKind::Async => write!(f, "__aexit__"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
enum ErrorKind {
|
||||
StarArgsNotAnnotated,
|
||||
MissingArgs,
|
||||
FirstArgBadAnnotation,
|
||||
SecondArgBadAnnotation,
|
||||
ThirdArgBadAnnotation,
|
||||
ArgsAfterFirstFourMustHaveDefault,
|
||||
AllKwargsMustHaveDefault,
|
||||
}
|
||||
|
||||
/// PYI036
|
||||
pub(crate) fn bad_exit_annotation(
|
||||
checker: &mut Checker,
|
||||
is_async: bool,
|
||||
name: &Identifier,
|
||||
args: &Arguments,
|
||||
) {
|
||||
let func_kind = match name.as_str() {
|
||||
"__exit__" if !is_async => FuncKind::Sync,
|
||||
"__aexit__" if is_async => FuncKind::Async,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let positional_args = args
|
||||
.args
|
||||
.iter()
|
||||
.chain(args.posonlyargs.iter())
|
||||
.collect::<SmallVec<[&ArgWithDefault; 4]>>();
|
||||
|
||||
// If there are less than three positional arguments, at least one of them must be a star-arg,
|
||||
// and it must be annotated with `object`.
|
||||
if positional_args.len() < 4 {
|
||||
check_short_args_list(checker, args, func_kind);
|
||||
}
|
||||
|
||||
// Every positional argument (beyond the first four) must have a default.
|
||||
for arg_with_default in positional_args
|
||||
.iter()
|
||||
.skip(4)
|
||||
.filter(|arg_with_default| arg_with_default.default.is_none())
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BadExitAnnotation {
|
||||
func_kind,
|
||||
error_kind: ErrorKind::ArgsAfterFirstFourMustHaveDefault,
|
||||
},
|
||||
arg_with_default.range(),
|
||||
));
|
||||
}
|
||||
|
||||
// ...as should all keyword-only arguments.
|
||||
for arg_with_default in args.kwonlyargs.iter().filter(|arg| arg.default.is_none()) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BadExitAnnotation {
|
||||
func_kind,
|
||||
error_kind: ErrorKind::AllKwargsMustHaveDefault,
|
||||
},
|
||||
arg_with_default.range(),
|
||||
));
|
||||
}
|
||||
|
||||
check_positional_args(checker, &positional_args, func_kind);
|
||||
}
|
||||
|
||||
/// Determine whether a "short" argument list (i.e., an argument list with less than four elements)
|
||||
/// contains a star-args argument annotated with `object`. If not, report an error.
|
||||
fn check_short_args_list(checker: &mut Checker, args: &Arguments, func_kind: FuncKind) {
|
||||
if let Some(varargs) = &args.vararg {
|
||||
if let Some(annotation) = varargs
|
||||
.annotation
|
||||
.as_ref()
|
||||
.filter(|ann| !is_object_or_unused(ann, checker.semantic()))
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BadExitAnnotation {
|
||||
func_kind,
|
||||
error_kind: ErrorKind::StarArgsNotAnnotated,
|
||||
},
|
||||
annotation.range(),
|
||||
);
|
||||
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if checker.semantic().is_builtin("object") {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
"object".to_string(),
|
||||
annotation.range(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
} else {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BadExitAnnotation {
|
||||
func_kind,
|
||||
error_kind: ErrorKind::MissingArgs,
|
||||
},
|
||||
args.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines whether the positional arguments of an `__exit__` or `__aexit__` method are
|
||||
/// annotated correctly.
|
||||
fn check_positional_args(
|
||||
checker: &mut Checker,
|
||||
positional_args: &[&ArgWithDefault],
|
||||
kind: FuncKind,
|
||||
) {
|
||||
// For each argument, define the predicate against which to check the annotation.
|
||||
type AnnotationValidator = fn(&Expr, &SemanticModel) -> bool;
|
||||
|
||||
let validations: [(ErrorKind, AnnotationValidator); 3] = [
|
||||
(ErrorKind::FirstArgBadAnnotation, is_base_exception_type),
|
||||
(ErrorKind::SecondArgBadAnnotation, is_base_exception),
|
||||
(ErrorKind::ThirdArgBadAnnotation, is_traceback_type),
|
||||
];
|
||||
|
||||
for (arg, (error_info, predicate)) in positional_args
|
||||
.iter()
|
||||
.skip(1)
|
||||
.take(3)
|
||||
.zip(validations.into_iter())
|
||||
{
|
||||
let Some(annotation) = arg.def.annotation.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if is_object_or_unused(annotation, checker.semantic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there's an annotation that's not `object` or `Unused`, check that the annotated type
|
||||
// matches the predicate.
|
||||
if non_none_annotation_element(annotation, checker.semantic())
|
||||
.map_or(false, |elem| predicate(elem, checker.semantic()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BadExitAnnotation {
|
||||
func_kind: kind,
|
||||
error_kind: error_info,
|
||||
},
|
||||
annotation.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the non-`None` annotation element of a PEP 604-style union or `Optional` annotation.
|
||||
fn non_none_annotation_element<'a>(
|
||||
annotation: &'a Expr,
|
||||
model: &SemanticModel,
|
||||
) -> Option<&'a Expr> {
|
||||
// E.g., `typing.Union` or `typing.Optional`
|
||||
if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation {
|
||||
if model.match_typing_expr(value, "Optional") {
|
||||
return if is_const_none(slice) {
|
||||
None
|
||||
} else {
|
||||
Some(slice)
|
||||
};
|
||||
}
|
||||
|
||||
if !model.match_typing_expr(value, "Union") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Expr::Tuple(ExprTuple { elts, .. }) = slice.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let [left, right] = elts.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
return match (is_const_none(left), is_const_none(right)) {
|
||||
(false, true) => Some(left),
|
||||
(true, false) => Some(right),
|
||||
(true, true) => None,
|
||||
(false, false) => None,
|
||||
};
|
||||
}
|
||||
|
||||
// PEP 604-style union (e.g., `int | None`)
|
||||
if let Expr::BinOp(ExprBinOp {
|
||||
op: Operator::BitOr,
|
||||
left,
|
||||
right,
|
||||
..
|
||||
}) = annotation
|
||||
{
|
||||
if !is_const_none(left) {
|
||||
return Some(left);
|
||||
}
|
||||
|
||||
if !is_const_none(right) {
|
||||
return Some(right);
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Expr`] is the `object` builtin or the `_typeshed.Unused` type.
|
||||
fn is_object_or_unused(expr: &Expr, model: &SemanticModel) -> bool {
|
||||
model
|
||||
.resolve_call_path(expr)
|
||||
.as_ref()
|
||||
.map_or(false, |call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["" | "builtins", "object"] | ["_typeshed", "Unused"]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Expr`] is `BaseException`.
|
||||
fn is_base_exception(expr: &Expr, model: &SemanticModel) -> bool {
|
||||
model
|
||||
.resolve_call_path(expr)
|
||||
.as_ref()
|
||||
.map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["" | "builtins", "BaseException"])
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Expr`] is the `types.TracebackType` type.
|
||||
fn is_traceback_type(expr: &Expr, model: &SemanticModel) -> bool {
|
||||
model
|
||||
.resolve_call_path(expr)
|
||||
.as_ref()
|
||||
.map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["types", "TracebackType"])
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Expr`] is, e.g., `Type[BaseException]`.
|
||||
fn is_base_exception_type(expr: &Expr, model: &SemanticModel) -> bool {
|
||||
let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if model.match_typing_expr(value, "Type")
|
||||
|| model
|
||||
.resolve_call_path(value)
|
||||
.as_ref()
|
||||
.map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["" | "builtins", "type"])
|
||||
})
|
||||
{
|
||||
is_base_exception(slice, model)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ pub(crate) use complex_if_statement_in_stub::*;
|
|||
pub(crate) use docstring_in_stubs::*;
|
||||
pub(crate) use duplicate_union_member::*;
|
||||
pub(crate) use ellipsis_in_non_empty_class_body::*;
|
||||
pub(crate) use exit_annotations::*;
|
||||
pub(crate) use future_annotations_in_stub::*;
|
||||
pub(crate) use iter_method_return_iterable::*;
|
||||
pub(crate) use no_return_argument_annotation::*;
|
||||
|
|
@ -33,6 +34,7 @@ mod complex_if_statement_in_stub;
|
|||
mod docstring_in_stubs;
|
||||
mod duplicate_union_member;
|
||||
mod ellipsis_in_non_empty_class_body;
|
||||
mod exit_annotations;
|
||||
mod future_annotations_in_stub;
|
||||
mod iter_method_return_iterable;
|
||||
mod no_return_argument_annotation;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI036.pyi:54:31: PYI036 [*] Star-args in `__exit__` should be annotated with `object`
|
||||
|
|
||||
53 | class BadOne:
|
||||
54 | def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation
|
||||
| ^^^ PYI036
|
||||
55 | async def __aexit__(self) -> None: ... # PYI036: Missing args
|
||||
|
|
||||
= help: Annotate star-args with `object`
|
||||
|
||||
ℹ Fix
|
||||
51 51 |
|
||||
52 52 |
|
||||
53 53 | class BadOne:
|
||||
54 |- def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation
|
||||
54 |+ def __exit__(self, *args: object) -> None: ... # PYI036: Bad star-args annotation
|
||||
55 55 | async def __aexit__(self) -> None: ... # PYI036: Missing args
|
||||
56 56 |
|
||||
57 57 | class BadTwo:
|
||||
|
||||
PYI036.pyi:55:24: PYI036 If there are no star-args, `__aexit__` should have at least 3 non-keyword-only args (excluding `self`)
|
||||
|
|
||||
53 | class BadOne:
|
||||
54 | def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation
|
||||
55 | async def __aexit__(self) -> None: ... # PYI036: Missing args
|
||||
| ^^^^^^ PYI036
|
||||
56 |
|
||||
57 | class BadTwo:
|
||||
|
|
||||
|
||||
PYI036.pyi:58:38: PYI036 All arguments after the first four in `__exit__` must have a default value
|
||||
|
|
||||
57 | class BadTwo:
|
||||
58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default
|
||||
| ^^^^^^^^^^^^^^^ PYI036
|
||||
59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default
|
||||
|
|
||||
|
||||
PYI036.pyi:59:48: PYI036 All keyword-only arguments in `__aexit__` must have a default value
|
||||
|
|
||||
57 | class BadTwo:
|
||||
58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default
|
||||
59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default
|
||||
| ^^^^^^^^^^^^^^^^ PYI036
|
||||
60 |
|
||||
61 | class BadThree:
|
||||
|
|
||||
|
||||
PYI036.pyi:59:66: PYI036 All keyword-only arguments in `__aexit__` must have a default value
|
||||
|
|
||||
57 | class BadTwo:
|
||||
58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default
|
||||
59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default
|
||||
| ^^^^^^^^^^^^^^^^ PYI036
|
||||
60 |
|
||||
61 | class BadThree:
|
||||
|
|
||||
|
||||
PYI036.pyi:62:29: PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None`
|
||||
|
|
||||
61 | class BadThree:
|
||||
62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation
|
||||
| ^^^^^^^^^^^^^^^^^^^ PYI036
|
||||
63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation
|
||||
|
|
||||
|
||||
PYI036.pyi:63:73: PYI036 The second argument in `__aexit__` should be annotated with `object` or `BaseException | None`
|
||||
|
|
||||
61 | class BadThree:
|
||||
62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation
|
||||
63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation
|
||||
| ^^^^^^^^^^^^^ PYI036
|
||||
64 |
|
||||
65 | class BadFour:
|
||||
|
|
||||
|
||||
PYI036.pyi:63:94: PYI036 The third argument in `__aexit__` should be annotated with `object` or `types.TracebackType | None`
|
||||
|
|
||||
61 | class BadThree:
|
||||
62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation
|
||||
63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation
|
||||
| ^^^^^^^^^^^^^ PYI036
|
||||
64 |
|
||||
65 | class BadFour:
|
||||
|
|
||||
|
||||
PYI036.pyi:66:111: PYI036 The third argument in `__exit__` should be annotated with `object` or `types.TracebackType | None`
|
||||
|
|
||||
65 | class BadFour:
|
||||
66 | def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation
|
||||
| ^^^^^^^^^^^^^ PYI036
|
||||
67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation
|
||||
|
|
||||
|
||||
PYI036.pyi:67:101: PYI036 The third argument in `__aexit__` should be annotated with `object` or `types.TracebackType | None`
|
||||
|
|
||||
65 | class BadFour:
|
||||
66 | def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation
|
||||
67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036
|
||||
68 |
|
||||
69 | class BadFive:
|
||||
|
|
||||
|
||||
PYI036.pyi:70:29: PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None`
|
||||
|
|
||||
69 | class BadFive:
|
||||
70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PYI036
|
||||
71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
|
||||
|
|
||||
|
||||
PYI036.pyi:70:58: PYI036 [*] Star-args in `__exit__` should be annotated with `object`
|
||||
|
|
||||
69 | class BadFive:
|
||||
70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
|
||||
| ^^^^^^^^^ PYI036
|
||||
71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
|
||||
|
|
||||
= help: Annotate star-args with `object`
|
||||
|
||||
ℹ Fix
|
||||
67 67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation
|
||||
68 68 |
|
||||
69 69 | class BadFive:
|
||||
70 |- def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
|
||||
70 |+ def __exit__(self, typ: BaseException | None, *args: object) -> bool: ... # PYI036: Bad star-args annotation
|
||||
71 71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
|
||||
72 72 |
|
||||
73 73 | class BadSix:
|
||||
|
||||
PYI036.pyi:71:74: PYI036 [*] Star-args in `__aexit__` should be annotated with `object`
|
||||
|
|
||||
69 | class BadFive:
|
||||
70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
|
||||
71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
|
||||
| ^^^ PYI036
|
||||
72 |
|
||||
73 | class BadSix:
|
||||
|
|
||||
= help: Annotate star-args with `object`
|
||||
|
||||
ℹ Fix
|
||||
68 68 |
|
||||
69 69 | class BadFive:
|
||||
70 70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation
|
||||
71 |- async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
|
||||
71 |+ async def __aexit__(self, /, typ: type[BaseException] | None, *args: object) -> Awaitable[None]: ... # PYI036: Bad star-args annotation
|
||||
72 72 |
|
||||
73 73 | class BadSix:
|
||||
74 74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
|
||||
|
||||
PYI036.pyi:74:38: PYI036 All arguments after the first four in `__exit__` must have a default value
|
||||
|
|
||||
73 | class BadSix:
|
||||
74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
|
||||
| ^^^^^^^^^^^^^^^ PYI036
|
||||
75 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default
|
||||
|
|
||||
|
||||
PYI036.pyi:75:48: PYI036 All keyword-only arguments in `__aexit__` must have a default value
|
||||
|
|
||||
73 | class BadSix:
|
||||
74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
|
||||
75 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default
|
||||
| ^^^^^^^^^^^^^^^ PYI036
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -2355,6 +2355,7 @@
|
|||
"PYI033",
|
||||
"PYI034",
|
||||
"PYI035",
|
||||
"PYI036",
|
||||
"PYI04",
|
||||
"PYI042",
|
||||
"PYI043",
|
||||
|
|
|
|||
Loading…
Reference in New Issue