From cfc64d1707ee424b9c8ac1d116d9820da7afc41b Mon Sep 17 00:00:00 2001 From: Bhuminjay Soni Date: Thu, 25 Sep 2025 23:22:24 +0530 Subject: [PATCH] [syntax-errors]: future-feature-not-defined (F407) (#20554) ## Summary This PR implements https://docs.astral.sh/ruff/rules/future-feature-not-defined/ (F407) as a semantic syntax error. ## Test Plan I have written inline tests as directed in #17412 --------- Signed-off-by: 11happy --- .../src/checkers/ast/analyze/statement.rs | 6 +- crates/ruff_linter/src/checkers/ast/mod.rs | 8 + .../rules/future_feature_not_defined.rs | 21 +-- .../inline/err/invalid_future_feature.py | 3 + .../inline/ok/valid_future_feature.py | 1 + .../ruff_python_parser/src/semantic_errors.rs | 46 +++++- ...alid_syntax@invalid_future_feature.py.snap | 146 ++++++++++++++++++ .../valid_syntax@valid_future_feature.py.snap | 42 +++++ crates/ruff_python_stdlib/src/future.rs | 17 -- crates/ruff_python_stdlib/src/lib.rs | 1 - 10 files changed, 246 insertions(+), 45 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/invalid_future_feature.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/valid_future_feature.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_future_feature.py.snap delete mode 100644 crates/ruff_python_stdlib/src/future.rs diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 503ecfb4ee..155836b3bd 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -803,11 +803,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } for alias in names { - if let Some("__future__") = module { - if checker.is_rule_enabled(Rule::FutureFeatureNotDefined) { - pyflakes::rules::future_feature_not_defined(checker, alias); - } - } else if &alias.name == "*" { + if module != Some("__future__") && &alias.name == "*" { // F403 checker.report_diagnostic_if_enabled( pyflakes::rules::UndefinedLocalWithImportStar { diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 66807415de..95014cbd1f 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -696,6 +696,14 @@ impl SemanticSyntaxContext for Checker<'_> { self.report_diagnostic(MultipleStarredExpressions, error.range); } } + SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => { + if self.is_rule_enabled(Rule::FutureFeatureNotDefined) { + self.report_diagnostic( + pyflakes::rules::FutureFeatureNotDefined { name }, + error.range, + ); + } + } SemanticSyntaxErrorKind::ReboundComprehensionVariable | SemanticSyntaxErrorKind::DuplicateTypeParameter | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs b/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs index f30347c0d6..9d5886f04a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs @@ -1,11 +1,6 @@ -use ruff_python_ast::Alias; - use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_stdlib::future::is_feature_name; -use ruff_text_size::Ranged; use crate::Violation; -use crate::checkers::ast::Checker; /// ## What it does /// Checks for `__future__` imports that are not defined in the current Python @@ -19,7 +14,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html) #[derive(ViolationMetadata)] pub(crate) struct FutureFeatureNotDefined { - name: String, + pub name: String, } impl Violation for FutureFeatureNotDefined { @@ -29,17 +24,3 @@ impl Violation for FutureFeatureNotDefined { format!("Future feature `{name}` is not defined") } } - -/// F407 -pub(crate) fn future_feature_not_defined(checker: &Checker, alias: &Alias) { - if is_feature_name(&alias.name) { - return; - } - - checker.report_diagnostic( - FutureFeatureNotDefined { - name: alias.name.to_string(), - }, - alias.range(), - ); -} diff --git a/crates/ruff_python_parser/resources/inline/err/invalid_future_feature.py b/crates/ruff_python_parser/resources/inline/err/invalid_future_feature.py new file mode 100644 index 0000000000..aa376ee3f3 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/invalid_future_feature.py @@ -0,0 +1,3 @@ +from __future__ import invalid_feature +from __future__ import annotations, invalid_feature +from __future__ import invalid_feature_1, invalid_feature_2 diff --git a/crates/ruff_python_parser/resources/inline/ok/valid_future_feature.py b/crates/ruff_python_parser/resources/inline/ok/valid_future_feature.py new file mode 100644 index 0000000000..9d48db4f9f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/valid_future_feature.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index b49e31ba5d..8913c5d0c1 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -65,8 +65,28 @@ impl SemanticSyntaxChecker { names, .. }) => { - if self.seen_futures_boundary && matches!(module.as_deref(), Some("__future__")) { - Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range); + if matches!(module.as_deref(), Some("__future__")) { + for name in names { + if !is_known_future_feature(&name.name) { + // test_ok valid_future_feature + // from __future__ import annotations + + // test_err invalid_future_feature + // from __future__ import invalid_feature + // from __future__ import annotations, invalid_feature + // from __future__ import invalid_feature_1, invalid_feature_2 + Self::add_error( + ctx, + SemanticSyntaxErrorKind::FutureFeatureNotDefined( + name.name.to_string(), + ), + name.range, + ); + } + } + if self.seen_futures_boundary { + Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range); + } } for alias in names { if alias.name.as_str() == "*" && !ctx.in_module_scope() { @@ -978,6 +998,22 @@ impl SemanticSyntaxChecker { } } +fn is_known_future_feature(name: &str) -> bool { + matches!( + name, + "nested_scopes" + | "generators" + | "division" + | "absolute_import" + | "with_statement" + | "print_function" + | "unicode_literals" + | "barry_as_FLUFL" + | "generator_stop" + | "annotations" + ) +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub struct SemanticSyntaxError { pub kind: SemanticSyntaxErrorKind, @@ -1086,6 +1122,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::MultipleStarredExpressions => { write!(f, "Two starred expressions in assignment") } + SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => { + write!(f, "Future feature `{name}` is not defined") + } } } } @@ -1456,6 +1495,9 @@ pub enum SemanticSyntaxErrorKind { /// left-hand side of an assignment. Using multiple starred expressions makes /// the statement invalid and results in a `SyntaxError`. MultipleStarredExpressions, + + /// Represents the use of a `__future__` feature that is not defined. + FutureFeatureNotDefined(String), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap new file mode 100644 index 0000000000..0de649a392 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap @@ -0,0 +1,146 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/invalid_future_feature.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..151, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 0..38, + module: Some( + Identifier { + id: Name("__future__"), + range: 5..15, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 23..38, + node_index: NodeIndex(None), + name: Identifier { + id: Name("invalid_feature"), + range: 23..38, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 39..90, + module: Some( + Identifier { + id: Name("__future__"), + range: 44..54, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 62..73, + node_index: NodeIndex(None), + name: Identifier { + id: Name("annotations"), + range: 62..73, + node_index: NodeIndex(None), + }, + asname: None, + }, + Alias { + range: 75..90, + node_index: NodeIndex(None), + name: Identifier { + id: Name("invalid_feature"), + range: 75..90, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 91..150, + module: Some( + Identifier { + id: Name("__future__"), + range: 96..106, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 114..131, + node_index: NodeIndex(None), + name: Identifier { + id: Name("invalid_feature_1"), + range: 114..131, + node_index: NodeIndex(None), + }, + asname: None, + }, + Alias { + range: 133..150, + node_index: NodeIndex(None), + name: Identifier { + id: Name("invalid_feature_2"), + range: 133..150, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | from __future__ import invalid_feature + | ^^^^^^^^^^^^^^^ Syntax Error: Future feature `invalid_feature` is not defined +2 | from __future__ import annotations, invalid_feature +3 | from __future__ import invalid_feature_1, invalid_feature_2 + | + + + | +1 | from __future__ import invalid_feature +2 | from __future__ import annotations, invalid_feature + | ^^^^^^^^^^^^^^^ Syntax Error: Future feature `invalid_feature` is not defined +3 | from __future__ import invalid_feature_1, invalid_feature_2 + | + + + | +1 | from __future__ import invalid_feature +2 | from __future__ import annotations, invalid_feature +3 | from __future__ import invalid_feature_1, invalid_feature_2 + | ^^^^^^^^^^^^^^^^^ Syntax Error: Future feature `invalid_feature_1` is not defined + | + + + | +1 | from __future__ import invalid_feature +2 | from __future__ import annotations, invalid_feature +3 | from __future__ import invalid_feature_1, invalid_feature_2 + | ^^^^^^^^^^^^^^^^^ Syntax Error: Future feature `invalid_feature_2` is not defined + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_future_feature.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_future_feature.py.snap new file mode 100644 index 0000000000..ea367acd4a --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_future_feature.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/valid_future_feature.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..35, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 0..34, + module: Some( + Identifier { + id: Name("__future__"), + range: 5..15, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 23..34, + node_index: NodeIndex(None), + name: Identifier { + id: Name("annotations"), + range: 23..34, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_stdlib/src/future.rs b/crates/ruff_python_stdlib/src/future.rs deleted file mode 100644 index b1adaadedf..0000000000 --- a/crates/ruff_python_stdlib/src/future.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// Returns `true` if `name` is a valid `__future__` feature name, as defined by -/// `__future__.all_feature_names`. -pub fn is_feature_name(name: &str) -> bool { - matches!( - name, - "nested_scopes" - | "generators" - | "division" - | "absolute_import" - | "with_statement" - | "print_function" - | "unicode_literals" - | "barry_as_FLUFL" - | "generator_stop" - | "annotations" - ) -} diff --git a/crates/ruff_python_stdlib/src/lib.rs b/crates/ruff_python_stdlib/src/lib.rs index a57f4315d6..632e9bb4e3 100644 --- a/crates/ruff_python_stdlib/src/lib.rs +++ b/crates/ruff_python_stdlib/src/lib.rs @@ -1,5 +1,4 @@ pub mod builtins; -pub mod future; pub mod identifiers; pub mod keyword; pub mod logging;