Skip to content

Commit c3f2187

Browse files
authored
[syntax-errors]: import from * only allowed at module scope (F406) (#20166)
<!-- 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 This PR implements F406 https://docs.astral.sh/ruff/rules/undefined-local-with-nested-import-star-usage/ as a semantic syntax error ## Test Plan I have written inline tests as directed in #17412 --------- Signed-off-by: 11happy <soni5happy@gmail.com>
1 parent 8a027b0 commit c3f2187

File tree

9 files changed

+373
-18
lines changed

9 files changed

+373
-18
lines changed

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use ruff_python_ast::helpers;
22
use ruff_python_ast::types::Node;
33
use ruff_python_ast::{self as ast, Expr, Stmt};
4-
use ruff_python_semantic::ScopeKind;
54
use ruff_text_size::Ranged;
65

76
use crate::checkers::ast::Checker;
@@ -809,17 +808,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
809808
pyflakes::rules::future_feature_not_defined(checker, alias);
810809
}
811810
} else if &alias.name == "*" {
812-
// F406
813-
if checker.is_rule_enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
814-
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
815-
checker.report_diagnostic(
816-
pyflakes::rules::UndefinedLocalWithNestedImportStarUsage {
817-
name: helpers::format_import_from(level, module).to_string(),
818-
},
819-
stmt.range(),
820-
);
821-
}
822-
}
823811
// F403
824812
checker.report_diagnostic_if_enabled(
825813
pyflakes::rules::UndefinedLocalWithImportStar {

crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub(crate) fn unresolved_references(checker: &Checker) {
1313

1414
for reference in checker.semantic.unresolved_references() {
1515
if reference.is_wildcard_import() {
16-
// F406
16+
// F405
1717
checker.report_diagnostic_if_enabled(
1818
pyflakes::rules::UndefinedLocalWithImportStarUsage {
1919
name: reference.name(checker.source()).to_string(),

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ use crate::package::PackageRoot;
6969
use crate::preview::is_undefined_export_in_dunder_init_enabled;
7070
use crate::registry::Rule;
7171
use crate::rules::pyflakes::rules::{
72-
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
72+
LateFutureImport, ReturnOutsideFunction, UndefinedLocalWithNestedImportStarUsage,
73+
YieldOutsideFunction,
7374
};
7475
use crate::rules::pylint::rules::{
7576
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
@@ -659,6 +660,14 @@ impl SemanticSyntaxContext for Checker<'_> {
659660
self.report_diagnostic(YieldOutsideFunction::new(kind), error.range);
660661
}
661662
}
663+
SemanticSyntaxErrorKind::NonModuleImportStar(name) => {
664+
if self.is_rule_enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
665+
self.report_diagnostic(
666+
UndefinedLocalWithNestedImportStarUsage { name },
667+
error.range,
668+
);
669+
}
670+
}
662671
SemanticSyntaxErrorKind::ReturnOutsideFunction => {
663672
// F706
664673
if self.is_rule_enabled(Rule::ReturnOutsideFunction) {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def f1():
2+
from module import *
3+
class C:
4+
from module import *
5+
def f2():
6+
from ..module import *
7+
def f3():
8+
from module import *, *
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from module import *

crates/ruff_python_parser/src/semantic_errors.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
//! This checker is not responsible for traversing the AST itself. Instead, its
44
//! [`SemanticSyntaxChecker::visit_stmt`] and [`SemanticSyntaxChecker::visit_expr`] methods should
55
//! be called in a parent `Visitor`'s `visit_stmt` and `visit_expr` methods, respectively.
6-
use std::fmt::Display;
7-
86
use ruff_python_ast::{
97
self as ast, Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr,
108
StmtImportFrom,
119
comparable::ComparableExpr,
10+
helpers,
1211
visitor::{Visitor, walk_expr},
1312
};
1413
use ruff_text_size::{Ranged, TextRange, TextSize};
1514
use rustc_hash::{FxBuildHasher, FxHashSet};
15+
use std::fmt::Display;
1616

1717
#[derive(Debug, Default)]
1818
pub struct SemanticSyntaxChecker {
@@ -58,10 +58,40 @@ impl SemanticSyntaxChecker {
5858

5959
fn check_stmt<Ctx: SemanticSyntaxContext>(&mut self, stmt: &ast::Stmt, ctx: &Ctx) {
6060
match stmt {
61-
Stmt::ImportFrom(StmtImportFrom { range, module, .. }) => {
61+
Stmt::ImportFrom(StmtImportFrom {
62+
range,
63+
module,
64+
level,
65+
names,
66+
..
67+
}) => {
6268
if self.seen_futures_boundary && matches!(module.as_deref(), Some("__future__")) {
6369
Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range);
6470
}
71+
for alias in names {
72+
if alias.name.as_str() == "*" && !ctx.in_module_scope() {
73+
// test_err import_from_star
74+
// def f1():
75+
// from module import *
76+
// class C:
77+
// from module import *
78+
// def f2():
79+
// from ..module import *
80+
// def f3():
81+
// from module import *, *
82+
83+
// test_ok import_from_star
84+
// from module import *
85+
Self::add_error(
86+
ctx,
87+
SemanticSyntaxErrorKind::NonModuleImportStar(
88+
helpers::format_import_from(*level, module.as_deref()).to_string(),
89+
),
90+
*range,
91+
);
92+
break;
93+
}
94+
}
6595
}
6696
Stmt::Match(match_stmt) => {
6797
Self::irrefutable_match_case(match_stmt, ctx);
@@ -1002,6 +1032,9 @@ impl Display for SemanticSyntaxError {
10021032
SemanticSyntaxErrorKind::YieldFromInAsyncFunction => {
10031033
f.write_str("`yield from` statement in async function; use `async for` instead")
10041034
}
1035+
SemanticSyntaxErrorKind::NonModuleImportStar(name) => {
1036+
write!(f, "`from {name} import *` only allowed at module level")
1037+
}
10051038
}
10061039
}
10071040
}
@@ -1362,6 +1395,9 @@ pub enum SemanticSyntaxErrorKind {
13621395

13631396
/// Represents the use of `yield from` inside an asynchronous function.
13641397
YieldFromInAsyncFunction,
1398+
1399+
/// Represents the use of `from <module> import *` outside module scope.
1400+
NonModuleImportStar(String),
13651401
}
13661402

13671403
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]

0 commit comments

Comments
 (0)