diff --git a/README.md b/README.md index 097744bc54..81f00ee3f3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/resources/test/fixtures/flake8_annotations/allow_star_arg_any.py b/resources/test/fixtures/flake8_annotations/allow_star_arg_any.py new file mode 100644 index 0000000000..55ead897e6 --- /dev/null +++ b/resources/test/fixtures/flake8_annotations/allow_star_arg_any.py @@ -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 diff --git a/resources/test/fixtures/flake8_annotations/annotation_presence.py b/resources/test/fixtures/flake8_annotations/annotation_presence.py index e7ab7f997a..200e1e24be 100644 --- a/resources/test/fixtures/flake8_annotations/annotation_presence.py +++ b/resources/test/fixtures/flake8_annotations/annotation_presence.py @@ -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: diff --git a/src/check_ast.rs b/src/check_ast.rs index b712a95a08..ff175e3bee 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -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); } diff --git a/src/checks.rs b/src/checks.rs index 0fe8b1597b..e780710644 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -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()) diff --git a/src/checks_gen.rs b/src/checks_gen.rs index 98c9b13181..46786795b0 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -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, diff --git a/src/flake8_annotations/mod.rs b/src/flake8_annotations/mod.rs index 038aeb37a9..db0877f926 100644 --- a/src/flake8_annotations/mod.rs +++ b/src/flake8_annotations/mod.rs @@ -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(()) + } } diff --git a/src/flake8_annotations/plugins.rs b/src/flake8_annotations/plugins.rs index d6c35e0e71..9d9914ac61 100644 --- a/src/flake8_annotations/plugins.rs +++ b/src/flake8_annotations/plugins.rs @@ -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>, &Vec) { 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 diff --git a/src/flake8_annotations/settings.rs b/src/flake8_annotations/settings.rs index 7930010b47..69f8d5049f 100644 --- a/src/flake8_annotations/settings.rs +++ b/src/flake8_annotations/settings.rs @@ -16,6 +16,8 @@ pub struct Options { /// - Explicit `return` statement(s) all return `None` (explicitly or /// implicitly). pub suppress_none_returning: Option, + /// Suppress ANN401 for dynamically typed *args and **kwargs. + pub allow_star_arg_any: Option, } #[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(), } } } diff --git a/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__allow_star_arg_any.snap b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__allow_star_arg_any.snap new file mode 100644 index 0000000000..ca6c094c16 --- /dev/null +++ b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__allow_star_arg_any.snap @@ -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: ~ + diff --git a/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__defaults.snap b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__defaults.snap index ad9a3c5e07..7ac71a587c 100644 --- a/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__defaults.snap +++ b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__defaults.snap @@ -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: ~