[syntax-errors]: future-feature-not-defined (F407) (#20554)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

This PR implements
https://docs.astral.sh/ruff/rules/future-feature-not-defined/ (F407) as
a semantic syntax error.

## Test Plan

<!-- How was it tested? -->

I have written inline tests as directed in #17412

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
This commit is contained in:
Bhuminjay Soni 2025-09-25 23:22:24 +05:30 committed by GitHub
parent 6b7a9dc2f2
commit cfc64d1707
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 246 additions and 45 deletions

View file

@ -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 {

View file

@ -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(_)

View file

@ -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(),
);
}

View file

@ -0,0 +1,3 @@
from __future__ import invalid_feature
from __future__ import annotations, invalid_feature
from __future__ import invalid_feature_1, invalid_feature_2

View file

@ -0,0 +1 @@
from __future__ import annotations

View file

@ -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)]

View file

@ -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
|

View file

@ -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,
},
),
],
},
)
```

View file

@ -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"
)
}

View file

@ -1,5 +1,4 @@
pub mod builtins;
pub mod future;
pub mod identifiers;
pub mod keyword;
pub mod logging;