Implement ANN401 (#657)

This commit is contained in:
Edgar R. M 2022-11-08 08:49:44 -06:00 committed by GitHub
parent 2ddc768412
commit 24aa177912
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 381 additions and 25 deletions

View File

@ -546,6 +546,7 @@ For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/2
| ANN204 | MissingReturnTypeMagicMethod | Missing return type annotation for magic method `...` | |
| ANN205 | MissingReturnTypeStaticMethod | Missing return type annotation for staticmethod `...` | |
| ANN206 | MissingReturnTypeClassMethod | Missing return type annotation for classmethod `...` | |
| ANN401 | DynamicallyTypedExpression | Dynamically typed expressions (typing.Any) are disallowed in `...` | |
### Ruff-specific rules

View File

@ -0,0 +1,57 @@
from typing import Any
# OK
def foo(a: int, *args: str, **kwargs: str) -> int:
pass
# ANN401
def foo(a: Any, *args: str, **kwargs: str) -> int:
pass
# ANN401
def foo(a: int, *args: str, **kwargs: str) -> Any:
pass
# OK
def foo(a: int, *args: Any, **kwargs: Any) -> int:
pass
# OK
def foo(a: int, *args: Any, **kwargs: str) -> int:
pass
# ANN401
def foo(a: int, *args: str, **kwargs: Any) -> int:
pass
class Bar:
# OK
def foo_method(self, a: int, *params: str, **options: str) -> int:
pass
# ANN401
def foo_method(self, a: Any, *params: str, **options: str) -> int:
pass
# ANN401
def foo_method(self, a: int, *params: str, **options: str) -> Any:
pass
# OK
def foo_method(self, a: int, *params: Any, **options: Any) -> int:
pass
# OK
def foo_method(self, a: int, *params: Any, **options: str) -> int:
pass
# OK
def foo_method(self, a: int, *params: str, **options: Any) -> int:
pass

View File

@ -1,4 +1,4 @@
from typing import Type
from typing import Any, Type
# Error
def foo(a, b):
@ -35,6 +35,36 @@ def foo() -> int:
pass
# OK
def foo(a: int, *args: str, **kwargs: str) -> int:
pass
# ANN401
def foo(a: Any, *args: str, **kwargs: str) -> int:
pass
# ANN401
def foo(a: int, *args: str, **kwargs: str) -> Any:
pass
# ANN401
def foo(a: int, *args: Any, **kwargs: Any) -> int:
pass
# ANN401
def foo(a: int, *args: Any, **kwargs: str) -> int:
pass
# ANN401
def foo(a: int, *args: str, **kwargs: Any) -> int:
pass
class Foo:
# OK
def foo(self: "Foo", a: int, b: int) -> int:
@ -44,6 +74,26 @@ class Foo:
def foo(self, a: int, b: int) -> int:
pass
# ANN401
def foo(self: "Foo", a: Any, *params: str, **options: str) -> int:
pass
# ANN401
def foo(self: "Foo", a: int, *params: str, **options: str) -> Any:
pass
# ANN401
def foo(self: "Foo", a: int, *params: Any, **options: Any) -> int:
pass
# ANN401
def foo(self: "Foo", a: int, *params: Any, **options: str) -> int:
pass
# ANN401
def foo(self: "Foo", a: int, *params: str, **options: Any) -> int:
pass
# OK
@classmethod
def foo(cls: Type["Foo"], a: int, b: int) -> int:

View File

@ -2408,6 +2408,7 @@ impl<'a> Checker<'a> {
|| self.settings.enabled.contains(&CheckCode::ANN204)
|| self.settings.enabled.contains(&CheckCode::ANN205)
|| self.settings.enabled.contains(&CheckCode::ANN206)
|| self.settings.enabled.contains(&CheckCode::ANN401)
{
flake8_annotations::plugins::definition(self, &definition, &visibility);
}

View File

@ -128,6 +128,7 @@ pub enum CheckCode {
ANN204,
ANN205,
ANN206,
ANN401,
// pyupgrade
U001,
U002,
@ -390,6 +391,7 @@ pub enum CheckKind {
MissingReturnTypeMagicMethod(String),
MissingReturnTypeStaticMethod(String),
MissingReturnTypeClassMethod(String),
DynamicallyTypedExpression(String),
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
@ -614,6 +616,7 @@ impl CheckCode {
CheckCode::ANN204 => CheckKind::MissingReturnTypeMagicMethod("...".to_string()),
CheckCode::ANN205 => CheckKind::MissingReturnTypeStaticMethod("...".to_string()),
CheckCode::ANN206 => CheckKind::MissingReturnTypeClassMethod("...".to_string()),
CheckCode::ANN401 => CheckKind::DynamicallyTypedExpression("...".to_string()),
// pyupgrade
CheckCode::U001 => CheckKind::UselessMetaclassType,
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
@ -810,6 +813,7 @@ impl CheckCode {
CheckCode::ANN204 => CheckCategory::Flake8Annotations,
CheckCode::ANN205 => CheckCategory::Flake8Annotations,
CheckCode::ANN206 => CheckCategory::Flake8Annotations,
CheckCode::ANN401 => CheckCategory::Flake8Annotations,
CheckCode::U001 => CheckCategory::Pyupgrade,
CheckCode::U002 => CheckCategory::Pyupgrade,
CheckCode::U003 => CheckCategory::Pyupgrade,
@ -993,6 +997,7 @@ impl CheckKind {
CheckKind::MissingReturnTypeMagicMethod(_) => &CheckCode::ANN204,
CheckKind::MissingReturnTypeStaticMethod(_) => &CheckCode::ANN205,
CheckKind::MissingReturnTypeClassMethod(_) => &CheckCode::ANN206,
CheckKind::DynamicallyTypedExpression(_) => &CheckCode::ANN401,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
@ -1416,6 +1421,9 @@ impl CheckKind {
CheckKind::MissingReturnTypeClassMethod(name) => {
format!("Missing return type annotation for classmethod `{name}`")
}
CheckKind::DynamicallyTypedExpression(name) => {
format!("Dynamically typed expressions (typing.Any) are disallowed in `{name}`")
}
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())

View File

@ -30,6 +30,9 @@ pub enum CheckCodePrefix {
ANN204,
ANN205,
ANN206,
ANN4,
ANN40,
ANN401,
B,
B0,
B00,
@ -290,6 +293,7 @@ impl CheckCodePrefix {
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
CheckCode::ANN401,
],
CheckCodePrefix::ANN0 => vec![CheckCode::ANN001, CheckCode::ANN002, CheckCode::ANN003],
CheckCodePrefix::ANN00 => vec![CheckCode::ANN001, CheckCode::ANN002, CheckCode::ANN003],
@ -319,6 +323,9 @@ impl CheckCodePrefix {
CheckCodePrefix::ANN204 => vec![CheckCode::ANN204],
CheckCodePrefix::ANN205 => vec![CheckCode::ANN205],
CheckCodePrefix::ANN206 => vec![CheckCode::ANN206],
CheckCodePrefix::ANN4 => vec![CheckCode::ANN401],
CheckCodePrefix::ANN40 => vec![CheckCode::ANN401],
CheckCodePrefix::ANN401 => vec![CheckCode::ANN401],
CheckCodePrefix::B => vec![
CheckCode::B002,
CheckCode::B003,
@ -1023,6 +1030,9 @@ impl CheckCodePrefix {
CheckCodePrefix::ANN204 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN205 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN206 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN4 => PrefixSpecificity::Hundreds,
CheckCodePrefix::ANN40 => PrefixSpecificity::Tens,
CheckCodePrefix::ANN401 => PrefixSpecificity::Explicit,
CheckCodePrefix::B => PrefixSpecificity::Category,
CheckCodePrefix::B0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::B00 => PrefixSpecificity::Tens,

View File

@ -45,6 +45,7 @@ mod tests {
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
CheckCode::ANN401,
])
},
&fixer::Mode::Generate,
@ -63,6 +64,7 @@ mod tests {
mypy_init_return: false,
suppress_dummy_args: true,
suppress_none_returning: false,
allow_star_arg_any: false,
},
..Settings::for_rules(vec![
CheckCode::ANN001,
@ -88,6 +90,7 @@ mod tests {
mypy_init_return: true,
suppress_dummy_args: false,
suppress_none_returning: false,
allow_star_arg_any: false,
},
..Settings::for_rules(vec![
CheckCode::ANN201,
@ -113,6 +116,7 @@ mod tests {
mypy_init_return: false,
suppress_dummy_args: false,
suppress_none_returning: true,
allow_star_arg_any: false,
},
..Settings::for_rules(vec![
CheckCode::ANN201,
@ -128,4 +132,24 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn allow_star_arg_any() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_annotations/allow_star_arg_any.py"),
&Settings {
flake8_annotations: flake8_annotations::settings::Settings {
mypy_init_return: false,
suppress_dummy_args: false,
suppress_none_returning: false,
allow_star_arg_any: true,
},
..Settings::for_rules(vec![CheckCode::ANN401])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@ -48,6 +48,16 @@ fn is_none_returning(body: &[Stmt]) -> bool {
true
}
/// ANN401
fn check_dynamically_typed(checker: &mut Checker, annotation: &Expr, name: &str) {
if checker.match_typing_module(annotation, "Any") {
checker.add_check(Check::new(
CheckKind::DynamicallyTypedExpression(name.to_string()),
Range::from_located(annotation),
));
};
}
fn match_function_def(stmt: &Stmt) -> (&str, &Arguments, &Option<Box<Expr>>, &Vec<Stmt>) {
match &stmt.node {
StmtKind::FunctionDef {
@ -81,14 +91,18 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => {
let (name, args, returns, body) = match_function_def(stmt);
// ANN001
// ANN001, ANN401
for arg in args
.args
.iter()
.chain(args.posonlyargs.iter())
.chain(args.kwonlyargs.iter())
{
if arg.node.annotation.is_none() {
if let Some(expr) = &arg.node.annotation {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, &arg.node.arg);
};
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
@ -102,9 +116,16 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
}
// ANN002
// ANN002, ANN401
if let Some(arg) = &args.vararg {
if arg.node.annotation.is_none() {
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("*{name}"));
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
@ -118,9 +139,16 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
}
// ANN003
// ANN003, ANN401
if let Some(arg) = &args.kwarg {
if arg.node.annotation.is_none() {
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("**{name}"));
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
@ -134,8 +162,12 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
}
// ANN201, ANN202
if returns.is_none() {
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, name);
};
} else {
// Allow omission of return annotation in `__init__` functions, if the function
// only returns `None` (explicitly or implicitly).
if checker.settings.flake8_annotations.suppress_none_returning
@ -179,7 +211,13 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
usize::from(!visibility::is_staticmethod(stmt)),
)
{
if arg.node.annotation.is_none() {
// ANN401 for dynamically typed arguments
if let Some(annotation) = &arg.node.annotation {
has_any_typed_arg = true;
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, annotation, &arg.node.arg);
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
@ -190,14 +228,20 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
));
}
}
} else {
has_any_typed_arg = true;
}
}
// ANN002
// ANN002, ANN401
if let Some(arg) = &args.vararg {
if arg.node.annotation.is_none() {
has_any_typed_arg = true;
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("*{name}"));
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
@ -208,14 +252,20 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
));
}
}
} else {
has_any_typed_arg = true;
}
}
// ANN003
// ANN003, ANN401
if let Some(arg) = &args.kwarg {
if arg.node.annotation.is_none() {
has_any_typed_arg = true;
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("**{name}"));
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
@ -226,8 +276,6 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
));
}
}
} else {
has_any_typed_arg = true;
}
}
@ -255,7 +303,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
// ANN201, ANN202
if returns.is_none() {
if let Some(expr) = &returns {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, name);
}
} else {
// Allow omission of return annotation in `__init__` functions, if the function
// only returns `None` (explicitly or implicitly).
if checker.settings.flake8_annotations.suppress_none_returning

View File

@ -16,6 +16,8 @@ pub struct Options {
/// - Explicit `return` statement(s) all return `None` (explicitly or
/// implicitly).
pub suppress_none_returning: Option<bool>,
/// Suppress ANN401 for dynamically typed *args and **kwargs.
pub allow_star_arg_any: Option<bool>,
}
#[derive(Debug, Hash, Default)]
@ -23,6 +25,7 @@ pub struct Settings {
pub mypy_init_return: bool,
pub suppress_dummy_args: bool,
pub suppress_none_returning: bool,
pub allow_star_arg_any: bool,
}
impl Settings {
@ -31,6 +34,7 @@ impl Settings {
mypy_init_return: options.mypy_init_return.unwrap_or_default(),
suppress_dummy_args: options.suppress_dummy_args.unwrap_or_default(),
suppress_none_returning: options.suppress_none_returning.unwrap_or_default(),
allow_star_arg_any: options.allow_star_arg_any.unwrap_or_default(),
}
}
}

View File

@ -0,0 +1,41 @@
---
source: src/flake8_annotations/mod.rs
expression: checks
---
- kind:
DynamicallyTypedExpression: a
location:
row: 10
column: 11
end_location:
row: 10
column: 14
fix: ~
- kind:
DynamicallyTypedExpression: foo
location:
row: 15
column: 46
end_location:
row: 15
column: 49
fix: ~
- kind:
DynamicallyTypedExpression: a
location:
row: 40
column: 28
end_location:
row: 40
column: 31
fix: ~
- kind:
DynamicallyTypedExpression: foo_method
location:
row: 44
column: 66
end_location:
row: 44
column: 69
fix: ~

View File

@ -75,21 +75,129 @@ expression: checks
column: 0
fix: ~
- kind:
MissingTypeSelf: self
DynamicallyTypedExpression: a
location:
row: 44
column: 12
column: 11
end_location:
row: 44
column: 14
fix: ~
- kind:
DynamicallyTypedExpression: foo
location:
row: 49
column: 46
end_location:
row: 49
column: 49
fix: ~
- kind:
DynamicallyTypedExpression: "*args"
location:
row: 54
column: 23
end_location:
row: 54
column: 26
fix: ~
- kind:
DynamicallyTypedExpression: "**kwargs"
location:
row: 54
column: 38
end_location:
row: 54
column: 41
fix: ~
- kind:
DynamicallyTypedExpression: "*args"
location:
row: 59
column: 23
end_location:
row: 59
column: 26
fix: ~
- kind:
DynamicallyTypedExpression: "**kwargs"
location:
row: 64
column: 38
end_location:
row: 64
column: 41
fix: ~
- kind:
MissingTypeSelf: self
location:
row: 74
column: 12
end_location:
row: 74
column: 16
fix: ~
- kind:
DynamicallyTypedExpression: a
location:
row: 78
column: 28
end_location:
row: 78
column: 31
fix: ~
- kind:
DynamicallyTypedExpression: foo
location:
row: 82
column: 66
end_location:
row: 82
column: 69
fix: ~
- kind:
DynamicallyTypedExpression: "*params"
location:
row: 86
column: 42
end_location:
row: 86
column: 45
fix: ~
- kind:
DynamicallyTypedExpression: "**options"
location:
row: 86
column: 58
end_location:
row: 86
column: 61
fix: ~
- kind:
DynamicallyTypedExpression: "*params"
location:
row: 90
column: 42
end_location:
row: 90
column: 45
fix: ~
- kind:
DynamicallyTypedExpression: "**options"
location:
row: 94
column: 58
end_location:
row: 94
column: 61
fix: ~
- kind:
MissingTypeCls: cls
location:
row: 54
row: 104
column: 12
end_location:
row: 54
row: 104
column: 15
fix: ~