Skip to content

Commit

Permalink
Check for Any in other types for ANN401 (#5601)
Browse files Browse the repository at this point in the history
## Summary

Check for `Any` in other types for `ANN401`. This reuses the logic from
`implicit-optional` rule to resolve the type to `Any`.

Following types are supported:
* `Union[Any, ...]`
* `Any | ...`
* `Optional[Any]`
* `Annotated[<any of the above variant>, ...]`
* Forward references i.e., `"Any | ..."`

## Test Plan

Added test cases for various combinations.

fixes: #5458
  • Loading branch information
dhruvmanila authored and konstin committed Jul 19, 2023
1 parent 731bffd commit 87c651f
Show file tree
Hide file tree
Showing 9 changed files with 458 additions and 378 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Type
from typing import Annotated, Any, Optional, Type, Union
from typing_extensions import override

# Error
Expand Down Expand Up @@ -95,27 +95,27 @@ def foo(self: "Foo", a: int, *params: Any, **options: str) -> int:
def foo(self: "Foo", a: int, *params: str, **options: Any) -> int:
pass

# ANN401
# OK
@override
def foo(self: "Foo", a: Any, *params: str, **options: str) -> int:
pass

# ANN401
# OK
@override
def foo(self: "Foo", a: int, *params: str, **options: str) -> Any:
pass

# ANN401
# OK
@override
def foo(self: "Foo", a: int, *params: Any, **options: Any) -> int:
pass

# ANN401
# OK
@override
def foo(self: "Foo", a: int, *params: Any, **options: str) -> int:
pass

# ANN401
# OK
@override
def foo(self: "Foo", a: int, *params: str, **options: Any) -> int:
pass
Expand All @@ -137,3 +137,17 @@ def foo(self, /, a: int, b: int) -> int:

# OK
def f(*args: *tuple[int]) -> None: ...
def f(a: object) -> None: ...
def f(a: str | bytes) -> None: ...
def f(a: Union[str, bytes]) -> None: ...
def f(a: Optional[str]) -> None: ...
def f(a: Annotated[str, ...]) -> None: ...
def f(a: "Union[str, bytes]") -> None: ...

# ANN401
def f(a: Any | int) -> None: ...
def f(a: int | Any) -> None: ...
def f(a: Union[str, bytes, Any]) -> None: ...
def f(a: Optional[Any]) -> None: ...
def f(a: Annotated[Any, ...]) -> None: ...
def f(a: "Union[str, bytes, Any]") -> None: ...
78 changes: 46 additions & 32 deletions crates/ruff/src/rules/flake8_annotations/rules/definition.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
use rustpython_parser::ast::{ArgWithDefault, Expr, Ranged, Stmt};
use rustpython_parser::ast::{self, ArgWithDefault, Constant, Expr, Ranged, Stmt};

use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::cast;
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::typing::parse_type_annotation;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel};
use ruff_python_semantic::{Definition, Member, MemberKind};
use ruff_python_stdlib::typing::simple_magic_return_type;

use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::ruff::typing::type_hint_resolves_to_any;

use super::super::fixes;
use super::super::helpers::match_function_def;
Expand Down Expand Up @@ -432,20 +434,46 @@ fn is_none_returning(body: &[Stmt]) -> bool {

/// ANN401
fn check_dynamically_typed<F>(
checker: &Checker,
annotation: &Expr,
func: F,
diagnostics: &mut Vec<Diagnostic>,
is_overridden: bool,
semantic: &SemanticModel,
) where
F: FnOnce() -> String,
{
if !is_overridden && semantic.match_typing_expr(annotation, "Any") {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
));
};
if let Expr::Constant(ast::ExprConstant {
range,
value: Constant::Str(string),
..
}) = annotation
{
// Quoted annotations
if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator) {
if type_hint_resolves_to_any(
&parsed_annotation,
checker.semantic(),
checker.locator,
checker.settings.target_version.minor(),
) {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
));
}
}
} else {
if type_hint_resolves_to_any(
annotation,
checker.semantic(),
checker.locator,
checker.settings.target_version.minor(),
) {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
));
}
}
}

/// Generate flake8-annotation checks for a given `Definition`.
Expand Down Expand Up @@ -500,13 +528,12 @@ pub(crate) fn definition(
// ANN401 for dynamically typed arguments
if let Some(annotation) = &def.annotation {
has_any_typed_arg = true;
if checker.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) && !is_overridden {
check_dynamically_typed(
checker,
annotation,
|| def.arg.to_string(),
&mut diagnostics,
is_overridden,
checker.semantic(),
);
}
} else {
Expand All @@ -530,15 +557,9 @@ pub(crate) fn definition(
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) && !is_overridden {
let name = &arg.arg;
check_dynamically_typed(
expr,
|| format!("*{name}"),
&mut diagnostics,
is_overridden,
checker.semantic(),
);
check_dynamically_typed(checker, expr, || format!("*{name}"), &mut diagnostics);
}
}
} else {
Expand All @@ -562,14 +583,13 @@ pub(crate) fn definition(
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.enabled(Rule::AnyType) {
if checker.enabled(Rule::AnyType) && !is_overridden {
let name = &arg.arg;
check_dynamically_typed(
checker,
expr,
|| format!("**{name}"),
&mut diagnostics,
is_overridden,
checker.semantic(),
);
}
}
Expand Down Expand Up @@ -629,14 +649,8 @@ pub(crate) fn definition(
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
has_typed_return = true;
if checker.enabled(Rule::AnyType) {
check_dynamically_typed(
expr,
|| name.to_string(),
&mut diagnostics,
is_overridden,
checker.semantic(),
);
if checker.enabled(Rule::AnyType) && !is_overridden {
check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics);
}
} else if !(
// Allow omission of return annotation if the function only returns `None`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,60 @@ annotation_presence.py:134:13: ANN101 Missing type annotation for `self` in meth
135 | pass
|

annotation_presence.py:148:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
147 | # ANN401
148 | def f(a: Any | int) -> None: ...
| ^^^^^^^^^ ANN401
149 | def f(a: int | Any) -> None: ...
150 | def f(a: Union[str, bytes, Any]) -> None: ...
|

annotation_presence.py:149:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
147 | # ANN401
148 | def f(a: Any | int) -> None: ...
149 | def f(a: int | Any) -> None: ...
| ^^^^^^^^^ ANN401
150 | def f(a: Union[str, bytes, Any]) -> None: ...
151 | def f(a: Optional[Any]) -> None: ...
|

annotation_presence.py:150:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
148 | def f(a: Any | int) -> None: ...
149 | def f(a: int | Any) -> None: ...
150 | def f(a: Union[str, bytes, Any]) -> None: ...
| ^^^^^^^^^^^^^^^^^^^^^^ ANN401
151 | def f(a: Optional[Any]) -> None: ...
152 | def f(a: Annotated[Any, ...]) -> None: ...
|

annotation_presence.py:151:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
149 | def f(a: int | Any) -> None: ...
150 | def f(a: Union[str, bytes, Any]) -> None: ...
151 | def f(a: Optional[Any]) -> None: ...
| ^^^^^^^^^^^^^ ANN401
152 | def f(a: Annotated[Any, ...]) -> None: ...
153 | def f(a: "Union[str, bytes, Any]") -> None: ...
|

annotation_presence.py:152:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
150 | def f(a: Union[str, bytes, Any]) -> None: ...
151 | def f(a: Optional[Any]) -> None: ...
152 | def f(a: Annotated[Any, ...]) -> None: ...
| ^^^^^^^^^^^^^^^^^^^ ANN401
153 | def f(a: "Union[str, bytes, Any]") -> None: ...
|

annotation_presence.py:153:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
151 | def f(a: Optional[Any]) -> None: ...
152 | def f(a: Annotated[Any, ...]) -> None: ...
153 | def f(a: "Union[str, bytes, Any]") -> None: ...
| ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401
|


1 change: 1 addition & 0 deletions crates/ruff/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Ruff-specific rules.
pub(crate) mod rules;
pub(crate) mod typing;

#[cfg(test)]
mod tests {
Expand Down
Loading

0 comments on commit 87c651f

Please sign in to comment.