Skip to content

Commit 3807ac5

Browse files
committed
implement F404 too
1 parent 7828bac commit 3807ac5

File tree

5 files changed

+74
-23
lines changed

5 files changed

+74
-23
lines changed

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4815,7 +4815,7 @@ impl<'db> TypeInferenceBuilder<'db> {
48154815
self.types.syntax_diagnostics.extend(
48164816
self.syntax_checker
48174817
.finish()
4818-
.map(|error| SyntaxDiagnostic::from_syntax_error(&error, file)),
4818+
.map(|error| SyntaxDiagnostic::from_syntax_error(error, file)),
48194819
);
48204820
self.types.shrink_to_fit();
48214821
self.types

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -852,14 +852,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
852852
if checker.enabled(Rule::FutureFeatureNotDefined) {
853853
pyflakes::rules::future_feature_not_defined(checker, alias);
854854
}
855-
if checker.enabled(Rule::LateFutureImport) {
856-
if checker.semantic.seen_futures_boundary() {
857-
checker.report_diagnostic(Diagnostic::new(
858-
pyflakes::rules::LateFutureImport,
859-
stmt.range(),
860-
));
861-
}
862-
}
863855
} else if &alias.name == "*" {
864856
if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
865857
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use std::path::Path;
2626

2727
use itertools::Itertools;
2828
use log::debug;
29-
use ruff_python_syntax_errors::SyntaxChecker;
29+
use ruff_python_syntax_errors::{SyntaxChecker, SyntaxError, SyntaxErrorKind};
3030
use rustc_hash::{FxHashMap, FxHashSet};
3131

3232
use ruff_diagnostics::{Diagnostic, IsolationLevel};
@@ -66,6 +66,7 @@ use crate::importer::Importer;
6666
use crate::noqa::NoqaMapping;
6767
use crate::package::PackageRoot;
6868
use crate::registry::Rule;
69+
use crate::rules::pyflakes::rules::LateFutureImport;
6970
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
7071
use crate::settings::{flags, LinterSettings};
7172
use crate::{docstrings, noqa, Locator};
@@ -2736,18 +2737,29 @@ pub(crate) fn check_ast(
27362737
checker.analyze.scopes.push(ScopeId::global());
27372738
analyze::deferred_scopes(&checker);
27382739

2739-
// checker.syntax_checker.
2740-
27412740
let mut diagnostics = checker.diagnostics.take();
27422741

2743-
diagnostics.extend(checker.syntax_checker.finish().map(|error| {
2744-
Diagnostic::new(
2742+
diagnostics.extend(
2743+
checker
2744+
.syntax_checker
2745+
.finish()
2746+
.filter_map(|error| try_diagnostic_from_syntax_error(error, &checker)),
2747+
);
2748+
2749+
diagnostics
2750+
}
2751+
2752+
fn try_diagnostic_from_syntax_error(error: &SyntaxError, checker: &Checker) -> Option<Diagnostic> {
2753+
match error.kind {
2754+
SyntaxErrorKind::MatchBeforePy310 => Some(Diagnostic::new(
27452755
crate::rules::syntax::VersionSyntaxError {
27462756
message: error.message(),
27472757
},
27482758
error.range,
2749-
)
2750-
}));
2751-
2752-
diagnostics
2759+
)),
2760+
SyntaxErrorKind::LateFutureImport if checker.enabled(Rule::LateFutureImport) => {
2761+
Some(Diagnostic::new(LateFutureImport, error.range))
2762+
}
2763+
SyntaxErrorKind::LateFutureImport => None,
2764+
}
27532765
}

crates/ruff_linter/src/rules/syntax/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub struct VersionSyntaxError {
77
}
88

99
impl Violation for VersionSyntaxError {
10+
#[allow(clippy::useless_format)]
1011
#[derive_message_formats]
1112
fn message(&self) -> String {
1213
format!("{}", self.message)

crates/ruff_python_syntax_errors/src/lib.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
//! a parent `Visitor`.
66
77
use ruff_python_ast::{
8-
visitor::source_order::{SourceOrderVisitor, TraversalSignal},
9-
AnyNodeRef, StmtMatch,
8+
visitor::source_order::{walk_stmt, SourceOrderVisitor, TraversalSignal},
9+
AnyNodeRef, Stmt, StmtExpr, StmtImportFrom, StmtMatch,
1010
};
1111
use ruff_text_size::TextRange;
1212

@@ -16,18 +16,24 @@ pub struct SyntaxChecker {
1616
target_version: PythonVersion,
1717
/// The cumulative set of syntax errors found when visiting the source AST.
1818
errors: Vec<SyntaxError>,
19+
20+
/// these could be grouped into a bitflags struct like `SemanticModel`
21+
seen_futures_boundary: bool,
22+
seen_docstring_boundary: bool,
1923
}
2024

2125
impl SyntaxChecker {
2226
pub fn new(target_version: PythonVersion) -> Self {
2327
Self {
2428
target_version,
2529
errors: Vec::new(),
30+
seen_futures_boundary: false,
31+
seen_docstring_boundary: false,
2632
}
2733
}
2834

29-
pub fn finish(self) -> impl Iterator<Item = SyntaxError> {
30-
self.errors.into_iter()
35+
pub fn finish(&self) -> impl Iterator<Item = &SyntaxError> {
36+
self.errors.iter()
3137
}
3238
}
3339

@@ -64,19 +70,24 @@ impl SyntaxError {
6470
major = self.target_version.major,
6571
minor = self.target_version.minor,
6672
),
73+
SyntaxErrorKind::LateFutureImport => {
74+
"__future__ imports must be at the top of the file".to_string()
75+
}
6776
}
6877
}
6978
}
7079

7180
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7281
pub enum SyntaxErrorKind {
7382
MatchBeforePy310,
83+
LateFutureImport,
7484
}
7585

7686
impl SyntaxErrorKind {
7787
pub const fn as_str(self) -> &'static str {
7888
match self {
7989
SyntaxErrorKind::MatchBeforePy310 => "match-before-python-310",
90+
SyntaxErrorKind::LateFutureImport => "late-future-import",
8091
}
8192
}
8293
}
@@ -93,6 +104,15 @@ impl<'a> SourceOrderVisitor<'a> for SyntaxChecker {
93104
});
94105
}
95106
}
107+
AnyNodeRef::StmtImportFrom(StmtImportFrom { range, module, .. }) => {
108+
if self.seen_futures_boundary && matches!(module.as_deref(), Some("__future__")) {
109+
self.errors.push(SyntaxError {
110+
kind: SyntaxErrorKind::LateFutureImport,
111+
range: *range,
112+
target_version: self.target_version,
113+
});
114+
}
115+
}
96116
AnyNodeRef::ModModule(_)
97117
| AnyNodeRef::ModExpression(_)
98118
| AnyNodeRef::StmtFunctionDef(_)
@@ -111,7 +131,6 @@ impl<'a> SourceOrderVisitor<'a> for SyntaxChecker {
111131
| AnyNodeRef::StmtTry(_)
112132
| AnyNodeRef::StmtAssert(_)
113133
| AnyNodeRef::StmtImport(_)
114-
| AnyNodeRef::StmtImportFrom(_)
115134
| AnyNodeRef::StmtGlobal(_)
116135
| AnyNodeRef::StmtNonlocal(_)
117136
| AnyNodeRef::StmtExpr(_)
@@ -187,6 +206,33 @@ impl<'a> SourceOrderVisitor<'a> for SyntaxChecker {
187206
}
188207
TraversalSignal::Skip
189208
}
209+
210+
fn visit_stmt(&mut self, stmt: &'a ruff_python_ast::Stmt) {
211+
match stmt {
212+
Stmt::Expr(StmtExpr { value, .. })
213+
if !self.seen_docstring_boundary && value.is_string_literal_expr() =>
214+
{
215+
self.seen_docstring_boundary = true;
216+
}
217+
Stmt::ImportFrom(StmtImportFrom { module, .. }) => {
218+
self.seen_docstring_boundary = true;
219+
// Allow __future__ imports until we see a non-__future__ import.
220+
if let Some("__future__") = module.as_deref() {
221+
} else {
222+
self.seen_futures_boundary = true;
223+
}
224+
}
225+
Stmt::Import(_) => {
226+
self.seen_docstring_boundary = true;
227+
self.seen_futures_boundary = true;
228+
}
229+
_ => {
230+
self.seen_docstring_boundary = true;
231+
self.seen_futures_boundary = true;
232+
}
233+
}
234+
walk_stmt(self, stmt);
235+
}
190236
}
191237

192238
#[cfg(test)]

0 commit comments

Comments
 (0)