diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 164df3846a..503ecfb4ee 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1,7 +1,6 @@ use ruff_python_ast::helpers; use ruff_python_ast::types::Node; use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::ScopeKind; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -809,17 +808,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pyflakes::rules::future_feature_not_defined(checker, alias); } } else if &alias.name == "*" { - // F406 - if checker.is_rule_enabled(Rule::UndefinedLocalWithNestedImportStarUsage) { - if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) { - checker.report_diagnostic( - pyflakes::rules::UndefinedLocalWithNestedImportStarUsage { - name: helpers::format_import_from(level, module).to_string(), - }, - stmt.range(), - ); - } - } // F403 checker.report_diagnostic_if_enabled( pyflakes::rules::UndefinedLocalWithImportStar { diff --git a/crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs b/crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs index 10ea851457..4a6764a119 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs @@ -13,7 +13,7 @@ pub(crate) fn unresolved_references(checker: &Checker) { for reference in checker.semantic.unresolved_references() { if reference.is_wildcard_import() { - // F406 + // F405 checker.report_diagnostic_if_enabled( pyflakes::rules::UndefinedLocalWithImportStarUsage { name: reference.name(checker.source()).to_string(), diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 4ee3ce4996..8dfe10a0ac 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -69,7 +69,8 @@ use crate::package::PackageRoot; use crate::preview::is_undefined_export_in_dunder_init_enabled; use crate::registry::Rule; use crate::rules::pyflakes::rules::{ - LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction, + LateFutureImport, ReturnOutsideFunction, UndefinedLocalWithNestedImportStarUsage, + YieldOutsideFunction, }; use crate::rules::pylint::rules::{ AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction, @@ -659,6 +660,14 @@ impl SemanticSyntaxContext for Checker<'_> { self.report_diagnostic(YieldOutsideFunction::new(kind), error.range); } } + SemanticSyntaxErrorKind::NonModuleImportStar(name) => { + if self.is_rule_enabled(Rule::UndefinedLocalWithNestedImportStarUsage) { + self.report_diagnostic( + UndefinedLocalWithNestedImportStarUsage { name }, + error.range, + ); + } + } SemanticSyntaxErrorKind::ReturnOutsideFunction => { // F706 if self.is_rule_enabled(Rule::ReturnOutsideFunction) { diff --git a/crates/ruff_python_parser/resources/inline/err/import_from_star.py b/crates/ruff_python_parser/resources/inline/err/import_from_star.py new file mode 100644 index 0000000000..303492501b --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/import_from_star.py @@ -0,0 +1,8 @@ +def f1(): + from module import * +class C: + from module import * +def f2(): + from ..module import * +def f3(): + from module import *, * diff --git a/crates/ruff_python_parser/resources/inline/ok/import_from_star.py b/crates/ruff_python_parser/resources/inline/ok/import_from_star.py new file mode 100644 index 0000000000..cbf8930347 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/import_from_star.py @@ -0,0 +1 @@ +from module import * diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 9b4d42ffd5..b5d6d65488 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -3,16 +3,16 @@ //! This checker is not responsible for traversing the AST itself. Instead, its //! [`SemanticSyntaxChecker::visit_stmt`] and [`SemanticSyntaxChecker::visit_expr`] methods should //! be called in a parent `Visitor`'s `visit_stmt` and `visit_expr` methods, respectively. -use std::fmt::Display; - use ruff_python_ast::{ self as ast, Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr, StmtImportFrom, comparable::ComparableExpr, + helpers, visitor::{Visitor, walk_expr}, }; use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::{FxBuildHasher, FxHashSet}; +use std::fmt::Display; #[derive(Debug, Default)] pub struct SemanticSyntaxChecker { @@ -58,10 +58,40 @@ impl SemanticSyntaxChecker { fn check_stmt(&mut self, stmt: &ast::Stmt, ctx: &Ctx) { match stmt { - Stmt::ImportFrom(StmtImportFrom { range, module, .. }) => { + Stmt::ImportFrom(StmtImportFrom { + range, + module, + level, + names, + .. + }) => { if self.seen_futures_boundary && matches!(module.as_deref(), Some("__future__")) { Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range); } + for alias in names { + if alias.name.as_str() == "*" && !ctx.in_module_scope() { + // test_err import_from_star + // def f1(): + // from module import * + // class C: + // from module import * + // def f2(): + // from ..module import * + // def f3(): + // from module import *, * + + // test_ok import_from_star + // from module import * + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonModuleImportStar( + helpers::format_import_from(*level, module.as_deref()).to_string(), + ), + *range, + ); + break; + } + } } Stmt::Match(match_stmt) => { Self::irrefutable_match_case(match_stmt, ctx); @@ -1002,6 +1032,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::YieldFromInAsyncFunction => { f.write_str("`yield from` statement in async function; use `async for` instead") } + SemanticSyntaxErrorKind::NonModuleImportStar(name) => { + write!(f, "`from {name} import *` only allowed at module level") + } } } } @@ -1362,6 +1395,9 @@ pub enum SemanticSyntaxErrorKind { /// Represents the use of `yield from` inside an asynchronous function. YieldFromInAsyncFunction, + + /// Represents the use of `from import *` outside module scope. + NonModuleImportStar(String), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap new file mode 100644 index 0000000000..2e1c53fdbd --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap @@ -0,0 +1,271 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/import_from_star.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..144, + body: [ + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 0..34, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f1"), + range: 4..6, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 6..8, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 14..34, + module: Some( + Identifier { + id: Name("module"), + range: 19..25, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 33..34, + node_index: NodeIndex(None), + name: Identifier { + id: Name("*"), + range: 33..34, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + node_index: NodeIndex(None), + range: 35..68, + decorator_list: [], + name: Identifier { + id: Name("C"), + range: 41..42, + node_index: NodeIndex(None), + }, + type_params: None, + arguments: None, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 48..68, + module: Some( + Identifier { + id: Name("module"), + range: 53..59, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 67..68, + node_index: NodeIndex(None), + name: Identifier { + id: Name("*"), + range: 67..68, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 69..105, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f2"), + range: 73..75, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 75..77, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 83..105, + module: Some( + Identifier { + id: Name("module"), + range: 90..96, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 104..105, + node_index: NodeIndex(None), + name: Identifier { + id: Name("*"), + range: 104..105, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 2, + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 106..143, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f3"), + range: 110..112, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 112..114, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 120..143, + module: Some( + Identifier { + id: Name("module"), + range: 125..131, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 139..140, + node_index: NodeIndex(None), + name: Identifier { + id: Name("*"), + range: 139..140, + node_index: NodeIndex(None), + }, + asname: None, + }, + Alias { + range: 142..143, + node_index: NodeIndex(None), + name: Identifier { + id: Name("*"), + range: 142..143, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + }, + ), + ], + }, + ), + ], + }, +) +``` +## Errors + + | +6 | from ..module import * +7 | def f3(): +8 | from module import *, * + | ^^^^ Syntax Error: Star import must be the only import + | + + +## Semantic Syntax Errors + + | +1 | def f1(): +2 | from module import * + | ^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from module import *` only allowed at module level +3 | class C: +4 | from module import * + | + + + | +2 | from module import * +3 | class C: +4 | from module import * + | ^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from module import *` only allowed at module level +5 | def f2(): +6 | from ..module import * + | + + + | +4 | from module import * +5 | def f2(): +6 | from ..module import * + | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from ..module import *` only allowed at module level +7 | def f3(): +8 | from module import *, * + | + + + | +6 | from ..module import * +7 | def f3(): +8 | from module import *, * + | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from module import *` only allowed at module level + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_from_star.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_from_star.py.snap new file mode 100644 index 0000000000..05896bd1e3 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_from_star.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/import_from_star.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..21, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 0..20, + module: Some( + Identifier { + id: Name("module"), + range: 5..11, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 19..20, + node_index: NodeIndex(None), + name: Identifier { + id: Name("*"), + range: 19..20, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + }, + ), + ], + }, +) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/star.md b/crates/ty_python_semantic/resources/mdtest/import/star.md index 3b4a2385b8..c66012b103 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/star.md +++ b/crates/ty_python_semantic/resources/mdtest/import/star.md @@ -1399,7 +1399,7 @@ X: bool = True ```py def f(): - # TODO: we should emit a syntax error here (tracked by https://github.com/astral-sh/ruff/issues/17412) + # error: [invalid-syntax] from exporter import * # error: [unresolved-reference]