Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,60 @@ def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) ->
@overload
def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036
def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...

class StringizedGoodOne:
def __exit__(self, *args: "object") -> None: ...
async def __aexit__(self, *args: "object") -> None: ...

class StringizedGoodTwo:
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",
) -> None: ...

class StringizedGoodThree:
def __exit__(self, typ: "object", exc: "object", tb: "object") -> None: ...
async def __aexit__(self, typ: object, exc: "object", tb: object) -> None: ...

class StringizedBadOne:
def __exit__(
self,
typ: "type[BaseException]",
exc: "BaseException | None",
tb: "TracebackType | None",
) -> None: ... # PYI036
async def __aexit__(
self,
typ: "type[BaseException] | None",
exc: "BaseException",
tb: "TracebackType | None",
) -> None: ... # PYI036

class StringizedGoodOverload:
@overload
def __exit__(self, exc_typ: "None", exc: "None", tb: "None") -> None: ...
@overload
def __exit__(
self,
exc_typ: "type[BaseException]",
exc: "BaseException",
tb: "TracebackType",
) -> None: ...
def __exit__(
self,
exc_typ: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None: ...

class StringizedBadVariadic:
def __exit__(self, *args: "str") -> None: ... # PYI036
async def __aexit__(self, *args: "int") -> None: ... # PYI036
37 changes: 22 additions & 15 deletions crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,11 @@ pub(crate) fn bad_exit_annotation(checker: &Checker, function: &StmtFunctionDef)
/// contains a star-args argument annotated with `object`. If not, report an error.
fn check_short_args_list(checker: &Checker, parameters: &Parameters, func_kind: FuncKind) {
if let Some(varargs) = &parameters.vararg {
if let Some(annotation) = varargs
.annotation()
.filter(|ann| !is_object_or_unused(ann, checker.semantic()))
{
if let Some(annotation) = varargs.annotation().filter(|ann| {
!checker.match_maybe_stringized_annotation(ann, |expr| {
is_object_or_unused(expr, checker.semantic())
})
}) {
let mut diagnostic = checker.report_diagnostic(
BadExitAnnotation {
func_kind,
Expand Down Expand Up @@ -272,15 +273,18 @@ fn check_positional_args_for_non_overloaded_method(
continue;
};

if is_object_or_unused(annotation, checker.semantic()) {
if checker.match_maybe_stringized_annotation(annotation, |expr| {
is_object_or_unused(expr, 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())
.is_some_and(|elem| predicate(elem, checker.semantic()))
{
if checker.match_maybe_stringized_annotation(annotation, |expr| {
non_none_annotation_element(expr, checker.semantic())
.is_some_and(|elem| predicate(elem, checker.semantic()))
}) {
continue;
}

Expand All @@ -306,10 +310,13 @@ fn check_positional_args_for_overloaded_method(
fn parameter_annotation_loosely_matches_predicate(
parameter: &ParameterWithDefault,
predicate: impl FnOnce(&Expr) -> bool,
semantic: &SemanticModel,
checker: &Checker,
) -> bool {
parameter.annotation().is_none_or(|annotation| {
predicate(annotation) || is_object_or_unused(annotation, semantic)
let Some(annotation) = parameter.annotation() else {
return true;
};
checker.match_maybe_stringized_annotation(annotation, |expr| {
predicate(expr) || is_object_or_unused(expr, checker.semantic())
})
}

Expand Down Expand Up @@ -400,7 +407,7 @@ fn check_positional_args_for_overloaded_method(
parameter_annotation_loosely_matches_predicate(
parameter,
Expr::is_none_literal_expr,
semantic,
checker,
)
}) {
return;
Expand All @@ -410,15 +417,15 @@ fn check_positional_args_for_overloaded_method(
if parameter_annotation_loosely_matches_predicate(
non_self_positional_args[0],
|annotation| is_base_exception_type(annotation, semantic),
semantic,
checker,
) && parameter_annotation_loosely_matches_predicate(
non_self_positional_args[1],
|annotation| semantic.match_builtin_expr(annotation, "BaseException"),
semantic,
checker,
) && parameter_annotation_loosely_matches_predicate(
non_self_positional_args[2],
|annotation| is_traceback_type(annotation, semantic),
semantic,
checker,
) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,55 @@ PYI036 Annotations for a three-argument `__exit__` overload (excluding `self`) s
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
169 | def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
|

PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None`
--> PYI036.py:196:14
|
194 | def __exit__(
195 | self,
196 | typ: "type[BaseException]",
| ^^^^^^^^^^^^^^^^^^^^^
197 | exc: "BaseException | None",
198 | tb: "TracebackType | None",
|

PYI036 The second argument in `__aexit__` should be annotated with `object` or `BaseException | None`
--> PYI036.py:203:14
|
201 | self,
202 | typ: "type[BaseException] | None",
203 | exc: "BaseException",
| ^^^^^^^^^^^^^^^
204 | tb: "TracebackType | None",
205 | ) -> None: ... # PYI036
|

PYI036 [*] Star-args in `__exit__` should be annotated with `object`
--> PYI036.py:225:31
|
224 | class StringizedBadVariadic:
225 | def __exit__(self, *args: "str") -> None: ... # PYI036
| ^^^^^
226 | async def __aexit__(self, *args: "int") -> None: ... # PYI036
|
help: Annotate star-args with `object`
|
224 | class StringizedBadVariadic:
- def __exit__(self, *args: "str") -> None: ... # PYI036
225 + def __exit__(self, *args: object) -> None: ... # PYI036
226 | async def __aexit__(self, *args: "int") -> None: ... # PYI036
|

PYI036 [*] Star-args in `__aexit__` should be annotated with `object`
--> PYI036.py:226:38
|
224 | class StringizedBadVariadic:
225 | def __exit__(self, *args: "str") -> None: ... # PYI036
226 | async def __aexit__(self, *args: "int") -> None: ... # PYI036
| ^^^^^
help: Annotate star-args with `object`
|
225 | def __exit__(self, *args: "str") -> None: ... # PYI036
- async def __aexit__(self, *args: "int") -> None: ... # PYI036
226 + async def __aexit__(self, *args: object) -> None: ... # PYI036
|
Loading