diff --git a/README.md b/README.md index a5a45e07c0..b103d34d89 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ ruff's goal is to achieve feature-parity with Flake8 when used (1) without any p stylistic checks; limiting to Python 3 obviates the need for certain compatibility checks.) Under those conditions, Flake8 implements about 58 rules, give or take. At time of writing, ruff -implements 30 rules. (Note that these 30 rules likely cover a disproportionate share of errors: +implements 31 rules. (Note that these 31 rules likely cover a disproportionate share of errors: unused imports, undefined variables, etc.) Of the unimplemented rules, ruff is missing: @@ -158,6 +158,7 @@ Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis F | F401 | UnusedImport | `...` imported but unused | | F403 | ImportStarUsage | Unable to detect undefined names | | F404 | LateFutureImport | from __future__ imports must occur at the beginning of the file | +| F407 | FutureFeatureNotDefined | future feature '...' is not defined | | F541 | FStringMissingPlaceholders | f-string without any placeholders | | F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | | F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | diff --git a/examples/generate_rules_table.rs b/examples/generate_rules_table.rs index 5be21fed7f..cd5f038101 100644 --- a/examples/generate_rules_table.rs +++ b/examples/generate_rules_table.rs @@ -9,6 +9,7 @@ fn main() { CheckKind::DoNotAssignLambda, CheckKind::DuplicateArgumentName, CheckKind::FStringMissingPlaceholders, + CheckKind::FutureFeatureNotDefined("...".to_string()), CheckKind::IOError("...".to_string()), CheckKind::IfTuple, CheckKind::ImportStarUsage, diff --git a/resources/test/fixtures/F407.py b/resources/test/fixtures/F407.py new file mode 100644 index 0000000000..08bf58ab50 --- /dev/null +++ b/resources/test/fixtures/F407.py @@ -0,0 +1,2 @@ +from __future__ import print_function +from __future__ import non_existent_feature diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index 9ba05911f2..8892c03189 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -14,6 +14,7 @@ select = [ "F401", "F403", "F404", + "F407", "F541", "F601", "F602", diff --git a/src/check_ast.rs b/src/check_ast.rs index 9dcede7a75..f84e8e0fcf 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -1,3 +1,4 @@ +use std::ops::Deref; use std::path::Path; use rustpython_parser::ast::{ @@ -14,6 +15,7 @@ use crate::ast::{checks, operations, visitor}; use crate::autofix::fixer; use crate::checks::{Check, CheckCode, CheckKind}; use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS}; +use crate::python::future::ALL_FEATURE_NAMES; use crate::python::typing; use crate::settings::Settings; @@ -391,7 +393,16 @@ where }, ); - if !self.futures_allowed && self.settings.select.contains(&CheckCode::F404) + if self.settings.select.contains(&CheckCode::F407) + && !ALL_FEATURE_NAMES.contains(&alias.node.name.deref()) + { + self.checks.push(Check::new( + CheckKind::FutureFeatureNotDefined(alias.node.name.to_string()), + stmt.location, + )); + } + + if self.settings.select.contains(&CheckCode::F404) && !self.futures_allowed { self.checks .push(Check::new(CheckKind::LateFutureImport, stmt.location)); diff --git a/src/checks.rs b/src/checks.rs index fcbcf10daa..54dd663e23 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -20,6 +20,7 @@ pub enum CheckCode { F401, F403, F404, + F407, F541, F601, F602, @@ -57,6 +58,7 @@ impl FromStr for CheckCode { "F401" => Ok(CheckCode::F401), "F403" => Ok(CheckCode::F403), "F404" => Ok(CheckCode::F404), + "F407" => Ok(CheckCode::F407), "F541" => Ok(CheckCode::F541), "F601" => Ok(CheckCode::F601), "F602" => Ok(CheckCode::F602), @@ -95,6 +97,7 @@ impl CheckCode { CheckCode::F401 => "F401", CheckCode::F403 => "F403", CheckCode::F404 => "F404", + CheckCode::F407 => "F407", CheckCode::F541 => "F541", CheckCode::F601 => "F601", CheckCode::F602 => "F602", @@ -131,6 +134,7 @@ impl CheckCode { CheckCode::F401 => &LintSource::AST, CheckCode::F403 => &LintSource::AST, CheckCode::F404 => &LintSource::AST, + CheckCode::F407 => &LintSource::AST, CheckCode::F541 => &LintSource::AST, CheckCode::F601 => &LintSource::AST, CheckCode::F602 => &LintSource::AST, @@ -174,6 +178,7 @@ pub enum CheckKind { DoNotAssignLambda, DuplicateArgumentName, FStringMissingPlaceholders, + FutureFeatureNotDefined(String), IOError(String), IfTuple, ImportStarUsage, @@ -209,6 +214,7 @@ impl CheckKind { CheckKind::DefaultExceptNotLast => "DefaultExceptNotLast", CheckKind::DuplicateArgumentName => "DuplicateArgumentName", CheckKind::FStringMissingPlaceholders => "FStringMissingPlaceholders", + CheckKind::FutureFeatureNotDefined(_) => "FutureFeatureNotDefined", CheckKind::IOError(_) => "IOError", CheckKind::IfTuple => "IfTuple", CheckKind::ImportStarUsage => "ImportStarUsage", @@ -246,6 +252,7 @@ impl CheckKind { CheckKind::DefaultExceptNotLast => &CheckCode::F707, CheckKind::DuplicateArgumentName => &CheckCode::F831, CheckKind::FStringMissingPlaceholders => &CheckCode::F541, + CheckKind::FutureFeatureNotDefined(_) => &CheckCode::F407, CheckKind::IOError(_) => &CheckCode::E902, CheckKind::IfTuple => &CheckCode::F634, CheckKind::ImportStarUsage => &CheckCode::F403, @@ -290,6 +297,9 @@ impl CheckKind { CheckKind::FStringMissingPlaceholders => { "f-string without any placeholders".to_string() } + CheckKind::FutureFeatureNotDefined(name) => { + format!("future feature '{name}' is not defined") + } CheckKind::IOError(name) => { format!("No such file or directory: `{name}`") } @@ -384,6 +394,7 @@ impl CheckKind { CheckKind::DoNotAssignLambda => false, CheckKind::DuplicateArgumentName => false, CheckKind::FStringMissingPlaceholders => false, + CheckKind::FutureFeatureNotDefined(_) => false, CheckKind::IOError(_) => false, CheckKind::IfTuple => false, CheckKind::ImportStarUsage => false, diff --git a/src/linter.rs b/src/linter.rs index a6bf22dcef..0f524147c5 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -535,6 +535,31 @@ mod tests { Ok(()) } + #[test] + fn f407() -> Result<()> { + let mut actual = check_path( + Path::new("./resources/test/fixtures/F407.py"), + &settings::Settings { + line_length: 88, + exclude: vec![], + select: BTreeSet::from([CheckCode::F407]), + }, + &fixer::Mode::Generate, + )?; + actual.sort_by_key(|check| check.location); + let expected = vec![Check { + kind: CheckKind::FutureFeatureNotDefined("non_existent_feature".to_string()), + location: Location::new(2, 1), + fix: None, + }]; + assert_eq!(actual.len(), expected.len()); + for i in 0..actual.len() { + assert_eq!(actual[i], expected[i]); + } + + Ok(()) + } + #[test] fn f541() -> Result<()> { let mut actual = check_path( diff --git a/src/pyproject.rs b/src/pyproject.rs index 0560af721f..80d4d56746 100644 --- a/src/pyproject.rs +++ b/src/pyproject.rs @@ -271,6 +271,7 @@ other-attribute = 1 CheckCode::F401, CheckCode::F403, CheckCode::F404, + CheckCode::F407, CheckCode::F541, CheckCode::F601, CheckCode::F602, diff --git a/src/python.rs b/src/python.rs index a84b39dbec..b68ea3f32d 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,2 +1,3 @@ pub mod builtins; +pub mod future; pub mod typing; diff --git a/src/python/future.rs b/src/python/future.rs new file mode 100644 index 0000000000..0e7bbb3c0c --- /dev/null +++ b/src/python/future.rs @@ -0,0 +1,13 @@ +/// A copy of `__future__.all_feature_names`. +pub const ALL_FEATURE_NAMES: &[&str] = &[ + "nested_scopes", + "generators", + "division", + "absolute_import", + "with_statement", + "print_function", + "unicode_literals", + "barry_as_FLUFL", + "generator_stop", + "annotations", +]; diff --git a/src/settings.rs b/src/settings.rs index 1b71e1644c..dc33b31897 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -55,6 +55,7 @@ impl Settings { CheckCode::E902, CheckCode::F401, CheckCode::F403, + CheckCode::F407, CheckCode::F541, CheckCode::F601, CheckCode::F602,