diff --git a/resources/test/fixtures/U006.py b/resources/test/fixtures/U006.py new file mode 100644 index 0000000000..db0d4332cb --- /dev/null +++ b/resources/test/fixtures/U006.py @@ -0,0 +1,12 @@ +from typing import List + + +def f(x: List[str]) -> None: + ... + + +import typing + + +def f(x: typing.List[str]) -> None: + ... diff --git a/src/check_ast.rs b/src/check_ast.rs index 4624cd803f..2091ea33df 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -731,7 +731,16 @@ where } } ExprKind::Name { id, ctx } => match ctx { - ExprContext::Load => self.handle_node_load(expr), + ExprContext::Load => { + // Ex) List[...] + if self.settings.enabled.contains(&CheckCode::U006) + && self.settings.target_version >= PythonVersion::Py39 + { + plugins::use_pep585_annotation(self, expr, id); + } + + self.handle_node_load(expr); + } ExprContext::Store => { if self.settings.enabled.contains(&CheckCode::E741) { if let Some(check) = checks::check_ambiguous_variable_name( @@ -748,6 +757,18 @@ where } ExprContext::Del => self.handle_node_delete(expr), }, + ExprKind::Attribute { value, attr, .. } => { + // Ex) typing.List[...] + if self.settings.enabled.contains(&CheckCode::U006) + && self.settings.target_version >= PythonVersion::Py39 + { + if let ExprKind::Name { id, .. } = &value.node { + if id == "typing" { + plugins::use_pep585_annotation(self, expr, attr); + } + } + } + } ExprKind::Call { func, args, diff --git a/src/checks.rs b/src/checks.rs index 9f3559322f..a08162d32f 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -140,6 +140,7 @@ pub enum CheckCode { U003, U004, U005, + U006, // Meta M001, } @@ -159,6 +160,7 @@ pub enum RejectedCmpop { #[derive(AsRefStr, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CheckKind { + // pycodestyle errors AmbiguousClassName(String), AmbiguousFunctionName(String), AmbiguousVariableName(String), @@ -201,7 +203,7 @@ pub enum CheckKind { UnusedImport(Vec), UnusedVariable(String), YieldOutsideFunction, - // More style + // pycodestyle warnings NoNewLineAtEndOfFile, // flake8-builtin BuiltinVariableShadowing(String), @@ -227,6 +229,7 @@ pub enum CheckKind { UselessMetaclassType, NoAssertEquals, UselessObjectInheritance(String), + UsePEP585Annotation(String), // Meta UnusedNOQA(Option), } @@ -318,6 +321,7 @@ impl CheckCode { CheckCode::U003 => CheckKind::TypeOfPrimitive(Primitive::Str), CheckCode::U004 => CheckKind::UselessObjectInheritance("...".to_string()), CheckCode::U005 => CheckKind::NoAssertEquals, + CheckCode::U006 => CheckKind::UsePEP585Annotation("List".to_string()), // Meta CheckCode::M001 => CheckKind::UnusedNOQA(None), } @@ -328,6 +332,7 @@ impl CheckKind { /// A four-letter shorthand code for the check. pub fn code(&self) -> &'static CheckCode { match self { + // pycodestyle errors CheckKind::AmbiguousClassName(_) => &CheckCode::E742, CheckKind::AmbiguousFunctionName(_) => &CheckCode::E743, CheckKind::AmbiguousVariableName(_) => &CheckCode::E741, @@ -370,7 +375,7 @@ impl CheckKind { CheckKind::UnusedImport(_) => &CheckCode::F401, CheckKind::UnusedVariable(_) => &CheckCode::F841, CheckKind::YieldOutsideFunction => &CheckCode::F704, - // More style + // pycodestyle warnings CheckKind::NoNewLineAtEndOfFile => &CheckCode::W292, // flake8-builtins CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001, @@ -395,6 +400,7 @@ impl CheckKind { CheckKind::UnnecessaryAbspath => &CheckCode::U002, CheckKind::UselessMetaclassType => &CheckCode::U001, CheckKind::NoAssertEquals => &CheckCode::U005, + CheckKind::UsePEP585Annotation(_) => &CheckCode::U006, CheckKind::UselessObjectInheritance(_) => &CheckCode::U004, // Meta CheckKind::UnusedNOQA(_) => &CheckCode::M001, @@ -404,6 +410,7 @@ impl CheckKind { /// The body text for the check. pub fn body(&self) -> String { match self { + // pycodestyle errors CheckKind::AmbiguousClassName(name) => { format!("Ambiguous class name: `{}`", name) } @@ -531,7 +538,7 @@ impl CheckKind { CheckKind::YieldOutsideFunction => { "`yield` or `yield from` statement outside of a function/method".to_string() } - // More style + // pycodestyle warnings CheckKind::NoNewLineAtEndOfFile => "No newline at end of file".to_string(), // flake8-builtins CheckKind::BuiltinVariableShadowing(name) => { @@ -589,6 +596,13 @@ impl CheckKind { CheckKind::UselessObjectInheritance(name) => { format!("Class `{name}` inherits from object") } + CheckKind::UsePEP585Annotation(name) => { + format!( + "Use `{}` instead of `{}` for type annotations", + name.to_lowercase(), + name, + ) + } // Meta CheckKind::UnusedNOQA(code) => match code { None => "Unused `noqa` directive".to_string(), @@ -611,6 +625,7 @@ impl CheckKind { | CheckKind::UnusedNOQA(_) | CheckKind::UselessMetaclassType | CheckKind::UselessObjectInheritance(_) + | CheckKind::UsePEP585Annotation(_) ) } } diff --git a/src/linter.rs b/src/linter.rs index e0a9448e89..a0f56502cc 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -989,4 +989,16 @@ mod tests { insta::assert_yaml_snapshot!(checks); Ok(()) } + + #[test] + fn u006() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/U006.py"), + &settings::Settings::for_rule(CheckCode::U006), + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } } diff --git a/src/plugins.rs b/src/plugins.rs index e4b8a442e5..fa46cd48a2 100644 --- a/src/plugins.rs +++ b/src/plugins.rs @@ -6,6 +6,7 @@ mod print_call; mod super_call_with_parameters; mod type_of_primitive; mod unnecessary_abspath; +mod use_pep585_annotation; mod useless_metaclass_type; mod useless_object_inheritance; @@ -17,5 +18,6 @@ pub use print_call::print_call; pub use super_call_with_parameters::super_call_with_parameters; pub use type_of_primitive::type_of_primitive; pub use unnecessary_abspath::unnecessary_abspath; +pub use use_pep585_annotation::use_pep585_annotation; pub use useless_metaclass_type::useless_metaclass_type; pub use useless_object_inheritance::useless_object_inheritance; diff --git a/src/plugins/use_pep585_annotation.rs b/src/plugins/use_pep585_annotation.rs new file mode 100644 index 0000000000..685c840953 --- /dev/null +++ b/src/plugins/use_pep585_annotation.rs @@ -0,0 +1,26 @@ +use rustpython_ast::Expr; + +use crate::ast::types::Range; +use crate::autofix::fixer; +use crate::check_ast::Checker; +use crate::checks::{Check, CheckKind, Fix}; +use crate::python::typing; + +pub fn use_pep585_annotation(checker: &mut Checker, expr: &Expr, id: &str) { + // TODO(charlie): Verify that the builtin is imported from the `typing` module. + if typing::is_pep585_builtin(id) { + let mut check = Check::new( + CheckKind::UsePEP585Annotation(id.to_string()), + Range::from_located(expr), + ); + if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) { + check.amend(Fix { + content: id.to_lowercase(), + location: expr.location, + end_location: expr.end_location, + applied: false, + }) + } + checker.add_check(check); + } +} diff --git a/src/python/typing.rs b/src/python/typing.rs index 171d5509e1..4707db24fe 100644 --- a/src/python/typing.rs +++ b/src/python/typing.rs @@ -92,3 +92,10 @@ pub fn is_annotated_subscript(name: &str) -> bool { pub fn is_pep593_annotated_subscript(name: &str) -> bool { name == "Annotated" } + +static PEP_585_BUILTINS: Lazy> = + Lazy::new(|| BTreeSet::from(["Dict", "FrozenSet", "List", "Set", "Tuple", "Type"])); + +pub fn is_pep585_builtin(name: &str) -> bool { + PEP_585_BUILTINS.contains(name) +} diff --git a/src/snapshots/ruff__linter__tests__u006.snap b/src/snapshots/ruff__linter__tests__u006.snap new file mode 100644 index 0000000000..14c6e5eeba --- /dev/null +++ b/src/snapshots/ruff__linter__tests__u006.snap @@ -0,0 +1,39 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: + UsePEP585Annotation: List + location: + row: 4 + column: 10 + end_location: + row: 4 + column: 14 + fix: + content: list + location: + row: 4 + column: 10 + end_location: + row: 4 + column: 14 + applied: false +- kind: + UsePEP585Annotation: List + location: + row: 11 + column: 10 + end_location: + row: 11 + column: 21 + fix: + content: list + location: + row: 11 + column: 10 + end_location: + row: 11 + column: 21 + applied: false +