diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index 801e6c6f8c..9697dc14d5 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -28,7 +28,6 @@ use crate::ast::{branch_detection, cast, helpers, operations, visitor}; use crate::docstrings::definition::{Definition, DefinitionKind, Docstring, Documentable}; use crate::noqa::Directive; use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS}; -use crate::python::future::ALL_FEATURE_NAMES; use crate::python::typing; use crate::python::typing::{Callable, SubscriptKind}; use crate::registry::{Diagnostic, Rule}; @@ -44,7 +43,6 @@ use crate::rules::{ use crate::settings::types::PythonVersion; use crate::settings::{flags, Settings}; use crate::source_code::{Indexer, Locator, Stylist}; -use crate::violations::DeferralKeyword; use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope}; use crate::{autofix, docstrings, noqa, violations, visibility}; @@ -55,7 +53,7 @@ type DeferralContext<'a> = (Vec, Vec>); #[allow(clippy::struct_excessive_bools)] pub struct Checker<'a> { // Input data. - path: &'a Path, + pub(crate) path: &'a Path, package: Option<&'a Path>, autofix: flags::Autofix, noqa: flags::Noqa, @@ -720,17 +718,7 @@ where } StmtKind::Return { .. } => { if self.settings.rules.enabled(&Rule::ReturnOutsideFunction) { - if let Some(&index) = self.scope_stack.last() { - if matches!( - self.scopes[index].kind, - ScopeKind::Class(_) | ScopeKind::Module - ) { - self.diagnostics.push(Diagnostic::new( - violations::ReturnOutsideFunction, - Range::from_located(stmt), - )); - } - } + pyflakes::rules::return_outside_function(self, stmt); } } StmtKind::ClassDef { @@ -1170,21 +1158,14 @@ where } if self.settings.rules.enabled(&Rule::FutureFeatureNotDefined) { - if !ALL_FEATURE_NAMES.contains(&&*alias.node.name) { - self.diagnostics.push(Diagnostic::new( - violations::FutureFeatureNotDefined { - name: alias.node.name.to_string(), - }, - Range::from_located(alias), - )); - } + pyflakes::rules::future_feature_not_defined(self, alias); } if self.settings.rules.enabled(&Rule::LateFutureImport) && !self.futures_allowed { self.diagnostics.push(Diagnostic::new( - violations::LateFutureImport, + pyflakes::rules::LateFutureImport, Range::from_located(stmt), )); } @@ -1207,7 +1188,7 @@ where [*(self.scope_stack.last().expect("No current scope found"))]; if !matches!(scope.kind, ScopeKind::Module) { self.diagnostics.push(Diagnostic::new( - violations::ImportStarNotPermitted { + pyflakes::rules::ImportStarNotPermitted { name: helpers::format_import_from( level.as_ref(), module.as_deref(), @@ -1220,7 +1201,7 @@ where if self.settings.rules.enabled(&Rule::ImportStarUsed) { self.diagnostics.push(Diagnostic::new( - violations::ImportStarUsed { + pyflakes::rules::ImportStarUsed { name: helpers::format_import_from( level.as_ref(), module.as_deref(), @@ -2197,7 +2178,7 @@ where .enabled(&Rule::StringDotFormatInvalidFormat) { self.diagnostics.push(Diagnostic::new( - violations::StringDotFormatInvalidFormat { + pyflakes::rules::StringDotFormatInvalidFormat { message: pyflakes::format::error_to_string(&e), }, location, @@ -2753,41 +2734,17 @@ where } ExprKind::Yield { .. } => { if self.settings.rules.enabled(&Rule::YieldOutsideFunction) { - let scope = self.current_scope(); - if matches!(scope.kind, ScopeKind::Class(_) | ScopeKind::Module) { - self.diagnostics.push(Diagnostic::new( - violations::YieldOutsideFunction { - keyword: DeferralKeyword::Yield, - }, - Range::from_located(expr), - )); - } + pyflakes::rules::yield_outside_function(self, expr); } } ExprKind::YieldFrom { .. } => { if self.settings.rules.enabled(&Rule::YieldOutsideFunction) { - let scope = self.current_scope(); - if matches!(scope.kind, ScopeKind::Class(_) | ScopeKind::Module) { - self.diagnostics.push(Diagnostic::new( - violations::YieldOutsideFunction { - keyword: DeferralKeyword::YieldFrom, - }, - Range::from_located(expr), - )); - } + pyflakes::rules::yield_outside_function(self, expr); } } ExprKind::Await { .. } => { if self.settings.rules.enabled(&Rule::YieldOutsideFunction) { - let scope = self.current_scope(); - if matches!(scope.kind, ScopeKind::Class(_) | ScopeKind::Module) { - self.diagnostics.push(Diagnostic::new( - violations::YieldOutsideFunction { - keyword: DeferralKeyword::Await, - }, - Range::from_located(expr), - )); - } + pyflakes::rules::yield_outside_function(self, expr); } if self.settings.rules.enabled(&Rule::AwaitOutsideAsync) { pylint::rules::await_outside_async(self, expr); @@ -2870,7 +2827,7 @@ where .enabled(&Rule::PercentFormatUnsupportedFormatCharacter) { self.diagnostics.push(Diagnostic::new( - violations::PercentFormatUnsupportedFormatCharacter { + pyflakes::rules::PercentFormatUnsupportedFormatCharacter { char: c, }, location, @@ -2884,7 +2841,7 @@ where .enabled(&Rule::PercentFormatInvalidFormat) { self.diagnostics.push(Diagnostic::new( - violations::PercentFormatInvalidFormat { + pyflakes::rules::PercentFormatInvalidFormat { message: e.to_string(), }, location, @@ -3574,7 +3531,7 @@ where if !self.bindings[*index].used() { if self.settings.rules.enabled(&Rule::UnusedVariable) { let mut diagnostic = Diagnostic::new( - violations::UnusedVariable { + pyflakes::rules::UnusedVariable { name: name.to_string(), }, name_range, @@ -3894,7 +3851,7 @@ impl<'a> Checker<'a> { if matches!(binding.kind, BindingKind::LoopVar) && existing_is_import { if self.settings.rules.enabled(&Rule::ImportShadowedByLoopVar) { self.diagnostics.push(Diagnostic::new( - violations::ImportShadowedByLoopVar { + pyflakes::rules::ImportShadowedByLoopVar { name: name.to_string(), line: existing.range.location.row(), }, @@ -3913,7 +3870,7 @@ impl<'a> Checker<'a> { { if self.settings.rules.enabled(&Rule::RedefinedWhileUnused) { self.diagnostics.push(Diagnostic::new( - violations::RedefinedWhileUnused { + pyflakes::rules::RedefinedWhileUnused { name: name.to_string(), line: existing.range.location.row(), }, @@ -4080,7 +4037,7 @@ impl<'a> Checker<'a> { from_list.sort(); self.diagnostics.push(Diagnostic::new( - violations::ImportStarUsage { + pyflakes::rules::ImportStarUsage { name: id.to_string(), sources: from_list, }, @@ -4114,7 +4071,7 @@ impl<'a> Checker<'a> { } self.diagnostics.push(Diagnostic::new( - violations::UndefinedName { name: id.clone() }, + pyflakes::rules::UndefinedName { name: id.clone() }, Range::from_located(expr), )); } @@ -4322,7 +4279,7 @@ impl<'a> Checker<'a> { && self.settings.rules.enabled(&Rule::UndefinedName) { self.diagnostics.push(Diagnostic::new( - violations::UndefinedName { + pyflakes::rules::UndefinedName { name: id.to_string(), }, Range::from_located(expr), @@ -4390,7 +4347,7 @@ impl<'a> Checker<'a> { .enabled(&Rule::ForwardAnnotationSyntaxError) { self.diagnostics.push(Diagnostic::new( - violations::ForwardAnnotationSyntaxError { + pyflakes::rules::ForwardAnnotationSyntaxError { body: expression.to_string(), }, range, @@ -4597,7 +4554,7 @@ impl<'a> Checker<'a> { for &name in names { if !scope.values.contains_key(name) { diagnostics.push(Diagnostic::new( - violations::UndefinedExport { + pyflakes::rules::UndefinedExport { name: name.to_string(), }, all_binding.range, @@ -4637,7 +4594,7 @@ impl<'a> Checker<'a> { if let Some(indices) = self.redefinitions.get(index) { for index in indices { diagnostics.push(Diagnostic::new( - violations::RedefinedWhileUnused { + pyflakes::rules::RedefinedWhileUnused { name: (*name).to_string(), line: binding.range.location.row(), }, @@ -4668,7 +4625,7 @@ impl<'a> Checker<'a> { for &name in names { if !scope.values.contains_key(name) { diagnostics.push(Diagnostic::new( - violations::ImportStarUsage { + pyflakes::rules::ImportStarUsage { name: name.to_string(), sources: from_list.clone(), }, @@ -4834,7 +4791,7 @@ impl<'a> Checker<'a> { let multiple = unused_imports.len() > 1; for (full_name, range) in unused_imports { let mut diagnostic = Diagnostic::new( - violations::UnusedImport { + pyflakes::rules::UnusedImport { name: full_name.to_string(), ignore_init, multiple, @@ -4860,7 +4817,7 @@ impl<'a> Checker<'a> { let multiple = unused_imports.len() > 1; for (full_name, range) in unused_imports { let mut diagnostic = Diagnostic::new( - violations::UnusedImport { + pyflakes::rules::UnusedImport { name: full_name.to_string(), ignore_init, multiple, diff --git a/src/noqa.rs b/src/noqa.rs index 91e31209ce..ec9512bae8 100644 --- a/src/noqa.rs +++ b/src/noqa.rs @@ -242,8 +242,8 @@ mod tests { use crate::noqa::{add_noqa_inner, NOQA_LINE_REGEX}; use crate::registry::Diagnostic; use crate::rules::pycodestyle::rules::AmbiguousVariableName; + use crate::rules::pyflakes; use crate::source_code::LineEnding; - use crate::violations; #[test] fn regex() { @@ -276,7 +276,7 @@ mod tests { assert_eq!(output, format!("{contents}\n")); let diagnostics = vec![Diagnostic::new( - violations::UnusedVariable { + pyflakes::rules::UnusedVariable { name: "x".to_string(), }, Range::new(Location::new(1, 0), Location::new(1, 0)), @@ -300,7 +300,7 @@ mod tests { Range::new(Location::new(1, 0), Location::new(1, 0)), ), Diagnostic::new( - violations::UnusedVariable { + pyflakes::rules::UnusedVariable { name: "x".to_string(), }, Range::new(Location::new(1, 0), Location::new(1, 0)), @@ -325,7 +325,7 @@ mod tests { Range::new(Location::new(1, 0), Location::new(1, 0)), ), Diagnostic::new( - violations::UnusedVariable { + pyflakes::rules::UnusedVariable { name: "x".to_string(), }, Range::new(Location::new(1, 0), Location::new(1, 0)), diff --git a/src/registry.rs b/src/registry.rs index fdc969ba9e..649fe6021f 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -33,49 +33,49 @@ ruff_macros::define_rule_mapping!( W505 => rules::pycodestyle::rules::DocLineTooLong, W605 => rules::pycodestyle::rules::InvalidEscapeSequence, // pyflakes - F401 => violations::UnusedImport, - F402 => violations::ImportShadowedByLoopVar, - F403 => violations::ImportStarUsed, - F404 => violations::LateFutureImport, - F405 => violations::ImportStarUsage, - F406 => violations::ImportStarNotPermitted, - F407 => violations::FutureFeatureNotDefined, - F501 => violations::PercentFormatInvalidFormat, - F502 => violations::PercentFormatExpectedMapping, - F503 => violations::PercentFormatExpectedSequence, - F504 => violations::PercentFormatExtraNamedArguments, - F505 => violations::PercentFormatMissingArgument, - F506 => violations::PercentFormatMixedPositionalAndNamed, - F507 => violations::PercentFormatPositionalCountMismatch, - F508 => violations::PercentFormatStarRequiresSequence, - F509 => violations::PercentFormatUnsupportedFormatCharacter, - F521 => violations::StringDotFormatInvalidFormat, - F522 => violations::StringDotFormatExtraNamedArguments, - F523 => violations::StringDotFormatExtraPositionalArguments, - F524 => violations::StringDotFormatMissingArguments, - F525 => violations::StringDotFormatMixingAutomatic, - F541 => violations::FStringMissingPlaceholders, - F601 => violations::MultiValueRepeatedKeyLiteral, - F602 => violations::MultiValueRepeatedKeyVariable, - F621 => violations::ExpressionsInStarAssignment, - F622 => violations::TwoStarredExpressions, - F631 => violations::AssertTuple, - F632 => violations::IsLiteral, - F633 => violations::InvalidPrintSyntax, - F634 => violations::IfTuple, - F701 => violations::BreakOutsideLoop, - F702 => violations::ContinueOutsideLoop, - F704 => violations::YieldOutsideFunction, - F706 => violations::ReturnOutsideFunction, - F707 => violations::DefaultExceptNotLast, - F722 => violations::ForwardAnnotationSyntaxError, - F811 => violations::RedefinedWhileUnused, - F821 => violations::UndefinedName, - F822 => violations::UndefinedExport, - F823 => violations::UndefinedLocal, - F841 => violations::UnusedVariable, - F842 => violations::UnusedAnnotation, - F901 => violations::RaiseNotImplemented, + F401 => rules::pyflakes::rules::UnusedImport, + F402 => rules::pyflakes::rules::ImportShadowedByLoopVar, + F403 => rules::pyflakes::rules::ImportStarUsed, + F404 => rules::pyflakes::rules::LateFutureImport, + F405 => rules::pyflakes::rules::ImportStarUsage, + F406 => rules::pyflakes::rules::ImportStarNotPermitted, + F407 => rules::pyflakes::rules::FutureFeatureNotDefined, + F501 => rules::pyflakes::rules::PercentFormatInvalidFormat, + F502 => rules::pyflakes::rules::PercentFormatExpectedMapping, + F503 => rules::pyflakes::rules::PercentFormatExpectedSequence, + F504 => rules::pyflakes::rules::PercentFormatExtraNamedArguments, + F505 => rules::pyflakes::rules::PercentFormatMissingArgument, + F506 => rules::pyflakes::rules::PercentFormatMixedPositionalAndNamed, + F507 => rules::pyflakes::rules::PercentFormatPositionalCountMismatch, + F508 => rules::pyflakes::rules::PercentFormatStarRequiresSequence, + F509 => rules::pyflakes::rules::PercentFormatUnsupportedFormatCharacter, + F521 => rules::pyflakes::rules::StringDotFormatInvalidFormat, + F522 => rules::pyflakes::rules::StringDotFormatExtraNamedArguments, + F523 => rules::pyflakes::rules::StringDotFormatExtraPositionalArguments, + F524 => rules::pyflakes::rules::StringDotFormatMissingArguments, + F525 => rules::pyflakes::rules::StringDotFormatMixingAutomatic, + F541 => rules::pyflakes::rules::FStringMissingPlaceholders, + F601 => rules::pyflakes::rules::MultiValueRepeatedKeyLiteral, + F602 => rules::pyflakes::rules::MultiValueRepeatedKeyVariable, + F621 => rules::pyflakes::rules::ExpressionsInStarAssignment, + F622 => rules::pyflakes::rules::TwoStarredExpressions, + F631 => rules::pyflakes::rules::AssertTuple, + F632 => rules::pyflakes::rules::IsLiteral, + F633 => rules::pyflakes::rules::InvalidPrintSyntax, + F634 => rules::pyflakes::rules::IfTuple, + F701 => rules::pyflakes::rules::BreakOutsideLoop, + F702 => rules::pyflakes::rules::ContinueOutsideLoop, + F704 => rules::pyflakes::rules::YieldOutsideFunction, + F706 => rules::pyflakes::rules::ReturnOutsideFunction, + F707 => rules::pyflakes::rules::DefaultExceptNotLast, + F722 => rules::pyflakes::rules::ForwardAnnotationSyntaxError, + F811 => rules::pyflakes::rules::RedefinedWhileUnused, + F821 => rules::pyflakes::rules::UndefinedName, + F822 => rules::pyflakes::rules::UndefinedExport, + F823 => rules::pyflakes::rules::UndefinedLocal, + F841 => rules::pyflakes::rules::UnusedVariable, + F842 => rules::pyflakes::rules::UnusedAnnotation, + F901 => rules::pyflakes::rules::RaiseNotImplemented, // pylint PLE0604 => rules::pylint::rules::InvalidAllObject, PLE0605 => rules::pylint::rules::InvalidAllFormat, diff --git a/src/rules/pyflakes/rules/assert_tuple.rs b/src/rules/pyflakes/rules/assert_tuple.rs index 4cea240e0b..52c661c51b 100644 --- a/src/rules/pyflakes/rules/assert_tuple.rs +++ b/src/rules/pyflakes/rules/assert_tuple.rs @@ -1,18 +1,29 @@ +use crate::define_violation; +use ruff_macros::derive_message_formats; use rustpython_ast::{Expr, ExprKind, Stmt}; use crate::ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::Diagnostic; -use crate::violations; +use crate::violation::Violation; + +define_violation!( + pub struct AssertTuple; +); +impl Violation for AssertTuple { + #[derive_message_formats] + fn message(&self) -> String { + format!("Assert test is a non-empty tuple, which is always `True`") + } +} /// F631 pub fn assert_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) { if let ExprKind::Tuple { elts, .. } = &test.node { if !elts.is_empty() { - checker.diagnostics.push(Diagnostic::new( - violations::AssertTuple, - Range::from_located(stmt), - )); + checker + .diagnostics + .push(Diagnostic::new(AssertTuple, Range::from_located(stmt))); } } } diff --git a/src/rules/pyflakes/rules/break_outside_loop.rs b/src/rules/pyflakes/rules/break_outside_loop.rs new file mode 100644 index 0000000000..4602faf80c --- /dev/null +++ b/src/rules/pyflakes/rules/break_outside_loop.rs @@ -0,0 +1,51 @@ +use crate::ast::types::Range; +use crate::define_violation; +use crate::registry::Diagnostic; + +use crate::violation::Violation; +use ruff_macros::derive_message_formats; +use rustpython_ast::{Stmt, StmtKind}; + +define_violation!( + pub struct BreakOutsideLoop; +); +impl Violation for BreakOutsideLoop { + #[derive_message_formats] + fn message(&self) -> String { + format!("`break` outside loop") + } +} + +/// F701 +pub fn break_outside_loop<'a>( + stmt: &'a Stmt, + parents: &mut impl Iterator, +) -> Option { + let mut allowed: bool = false; + let mut child = stmt; + for parent in parents { + match &parent.node { + StmtKind::For { orelse, .. } + | StmtKind::AsyncFor { orelse, .. } + | StmtKind::While { orelse, .. } => { + if !orelse.contains(child) { + allowed = true; + break; + } + } + StmtKind::FunctionDef { .. } + | StmtKind::AsyncFunctionDef { .. } + | StmtKind::ClassDef { .. } => { + break; + } + _ => {} + } + child = parent; + } + + if allowed { + None + } else { + Some(Diagnostic::new(BreakOutsideLoop, Range::from_located(stmt))) + } +} diff --git a/src/rules/pyflakes/rules/continue_outside_loop.rs b/src/rules/pyflakes/rules/continue_outside_loop.rs new file mode 100644 index 0000000000..7c37faeadc --- /dev/null +++ b/src/rules/pyflakes/rules/continue_outside_loop.rs @@ -0,0 +1,54 @@ +use crate::ast::types::Range; +use crate::define_violation; +use crate::registry::Diagnostic; + +use crate::violation::Violation; +use ruff_macros::derive_message_formats; +use rustpython_ast::{Stmt, StmtKind}; + +define_violation!( + pub struct ContinueOutsideLoop; +); +impl Violation for ContinueOutsideLoop { + #[derive_message_formats] + fn message(&self) -> String { + format!("`continue` not properly in loop") + } +} + +/// F702 +pub fn continue_outside_loop<'a>( + stmt: &'a Stmt, + parents: &mut impl Iterator, +) -> Option { + let mut allowed: bool = false; + let mut child = stmt; + for parent in parents { + match &parent.node { + StmtKind::For { orelse, .. } + | StmtKind::AsyncFor { orelse, .. } + | StmtKind::While { orelse, .. } => { + if !orelse.contains(child) { + allowed = true; + break; + } + } + StmtKind::FunctionDef { .. } + | StmtKind::AsyncFunctionDef { .. } + | StmtKind::ClassDef { .. } => { + break; + } + _ => {} + } + child = parent; + } + + if allowed { + None + } else { + Some(Diagnostic::new( + ContinueOutsideLoop, + Range::from_located(stmt), + )) + } +} diff --git a/src/rules/pyflakes/rules/default_except_not_last.rs b/src/rules/pyflakes/rules/default_except_not_last.rs new file mode 100644 index 0000000000..9ac83e948a --- /dev/null +++ b/src/rules/pyflakes/rules/default_except_not_last.rs @@ -0,0 +1,36 @@ +use crate::ast::helpers::except_range; +use crate::define_violation; +use crate::registry::Diagnostic; +use crate::source_code::Locator; + +use crate::violation::Violation; +use ruff_macros::derive_message_formats; +use rustpython_ast::{Excepthandler, ExcepthandlerKind}; + +define_violation!( + pub struct DefaultExceptNotLast; +); +impl Violation for DefaultExceptNotLast { + #[derive_message_formats] + fn message(&self) -> String { + format!("An `except` block as not the last exception handler") + } +} + +/// F707 +pub fn default_except_not_last( + handlers: &[Excepthandler], + locator: &Locator, +) -> Option { + for (idx, handler) in handlers.iter().enumerate() { + let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node; + if type_.is_none() && idx < handlers.len() - 1 { + return Some(Diagnostic::new( + DefaultExceptNotLast, + except_range(handler, locator), + )); + } + } + + None +} diff --git a/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index 9e0989583a..b4ec8af00a 100644 --- a/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -1,10 +1,26 @@ +use crate::define_violation; +use ruff_macros::derive_message_formats; use rustpython_ast::{Expr, ExprKind}; use crate::ast::helpers::find_useless_f_strings; use crate::checkers::ast::Checker; use crate::fix::Fix; use crate::registry::Diagnostic; -use crate::violations; +use crate::violation::AlwaysAutofixableViolation; + +define_violation!( + pub struct FStringMissingPlaceholders; +); +impl AlwaysAutofixableViolation for FStringMissingPlaceholders { + #[derive_message_formats] + fn message(&self) -> String { + format!("f-string without any placeholders") + } + + fn autofix_title(&self) -> String { + "Remove extraneous `f` prefix".to_string() + } +} /// F541 pub fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) { @@ -13,7 +29,7 @@ pub fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut .any(|value| matches!(value.node, ExprKind::FormattedValue { .. })) { for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator) { - let mut diagnostic = Diagnostic::new(violations::FStringMissingPlaceholders, tok_range); + let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range); if checker.patch(diagnostic.kind.rule()) { diagnostic.amend(Fix::deletion( prefix_range.location, diff --git a/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs b/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs new file mode 100644 index 0000000000..ea0f7fbd06 --- /dev/null +++ b/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs @@ -0,0 +1,16 @@ +use crate::define_violation; +use crate::violation::Violation; +use ruff_macros::derive_message_formats; + +define_violation!( + pub struct ForwardAnnotationSyntaxError { + pub body: String, + } +); +impl Violation for ForwardAnnotationSyntaxError { + #[derive_message_formats] + fn message(&self) -> String { + let ForwardAnnotationSyntaxError { body } = self; + format!("Syntax error in forward annotation: `{body}`") + } +} diff --git a/src/rules/pyflakes/rules/if_tuple.rs b/src/rules/pyflakes/rules/if_tuple.rs index 8a1c456585..edf8f1b986 100644 --- a/src/rules/pyflakes/rules/if_tuple.rs +++ b/src/rules/pyflakes/rules/if_tuple.rs @@ -1,18 +1,29 @@ +use crate::define_violation; +use ruff_macros::derive_message_formats; use rustpython_ast::{Expr, ExprKind, Stmt}; use crate::ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::Diagnostic; -use crate::violations; +use crate::violation::Violation; + +define_violation!( + pub struct IfTuple; +); +impl Violation for IfTuple { + #[derive_message_formats] + fn message(&self) -> String { + format!("If test is a tuple, which is always `True`") + } +} /// F634 pub fn if_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) { if let ExprKind::Tuple { elts, .. } = &test.node { if !elts.is_empty() { - checker.diagnostics.push(Diagnostic::new( - violations::IfTuple, - Range::from_located(stmt), - )); + checker + .diagnostics + .push(Diagnostic::new(IfTuple, Range::from_located(stmt))); } } } diff --git a/src/rules/pyflakes/rules/imports.rs b/src/rules/pyflakes/rules/imports.rs new file mode 100644 index 0000000000..dda47ca283 --- /dev/null +++ b/src/rules/pyflakes/rules/imports.rs @@ -0,0 +1,143 @@ +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::python::future::ALL_FEATURE_NAMES; +use crate::registry::Diagnostic; +use crate::violation::{Availability, Violation}; +use crate::{define_violation, AutofixKind}; +use itertools::Itertools; +use ruff_macros::derive_message_formats; +use rustpython_ast::Alias; + +define_violation!( + pub struct UnusedImport { + pub name: String, + pub ignore_init: bool, + pub multiple: bool, + } +); +fn fmt_unused_import_autofix_msg(unused_import: &UnusedImport) -> String { + let UnusedImport { name, multiple, .. } = unused_import; + if *multiple { + "Remove unused import".to_string() + } else { + format!("Remove unused import: `{name}`") + } +} +impl Violation for UnusedImport { + const AUTOFIX: Option = Some(AutofixKind::new(Availability::Always)); + + #[derive_message_formats] + fn message(&self) -> String { + let UnusedImport { + name, ignore_init, .. + } = self; + if *ignore_init { + format!( + "`{name}` imported but unused; consider adding to `__all__` or using a redundant \ + alias" + ) + } else { + format!("`{name}` imported but unused") + } + } + + fn autofix_title_formatter(&self) -> Option String> { + let UnusedImport { ignore_init, .. } = self; + if *ignore_init { + None + } else { + Some(fmt_unused_import_autofix_msg) + } + } +} +define_violation!( + pub struct ImportShadowedByLoopVar { + pub name: String, + pub line: usize, + } +); +impl Violation for ImportShadowedByLoopVar { + #[derive_message_formats] + fn message(&self) -> String { + let ImportShadowedByLoopVar { name, line } = self; + format!("Import `{name}` from line {line} shadowed by loop variable") + } +} + +define_violation!( + pub struct ImportStarUsed { + pub name: String, + } +); +impl Violation for ImportStarUsed { + #[derive_message_formats] + fn message(&self) -> String { + let ImportStarUsed { name } = self; + format!("`from {name} import *` used; unable to detect undefined names") + } +} + +define_violation!( + pub struct LateFutureImport; +); +impl Violation for LateFutureImport { + #[derive_message_formats] + fn message(&self) -> String { + format!("`from __future__` imports must occur at the beginning of the file") + } +} + +define_violation!( + pub struct ImportStarUsage { + pub name: String, + pub sources: Vec, + } +); +impl Violation for ImportStarUsage { + #[derive_message_formats] + fn message(&self) -> String { + let ImportStarUsage { name, sources } = self; + let sources = sources + .iter() + .map(|source| format!("`{source}`")) + .join(", "); + format!("`{name}` may be undefined, or defined from star imports: {sources}") + } +} + +define_violation!( + pub struct ImportStarNotPermitted { + pub name: String, + } +); +impl Violation for ImportStarNotPermitted { + #[derive_message_formats] + fn message(&self) -> String { + let ImportStarNotPermitted { name } = self; + format!("`from {name} import *` only allowed at module level") + } +} + +define_violation!( + pub struct FutureFeatureNotDefined { + pub name: String, + } +); +impl Violation for FutureFeatureNotDefined { + #[derive_message_formats] + fn message(&self) -> String { + let FutureFeatureNotDefined { name } = self; + format!("Future feature `{name}` is not defined") + } +} + +pub fn future_feature_not_defined(checker: &mut Checker, alias: &Alias) { + if !ALL_FEATURE_NAMES.contains(&&*alias.node.name) { + checker.diagnostics.push(Diagnostic::new( + FutureFeatureNotDefined { + name: alias.node.name.to_string(), + }, + Range::from_located(alias), + )); + } +} diff --git a/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index 7e29555731..add31f1190 100644 --- a/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -1,14 +1,56 @@ -use itertools::izip; -use once_cell::unsync::Lazy; -use rustpython_ast::{Cmpop, Expr}; - use crate::ast::helpers; use crate::ast::operations::locate_cmpops; use crate::ast::types::Range; use crate::checkers::ast::Checker; +use crate::define_violation; use crate::fix::Fix; use crate::registry::Diagnostic; -use crate::violations; +use crate::violation::AlwaysAutofixableViolation; +use itertools::izip; +use once_cell::unsync::Lazy; +use ruff_macros::derive_message_formats; +use rustpython_ast::{Cmpop, Expr}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum IsCmpop { + Is, + IsNot, +} + +impl From<&Cmpop> for IsCmpop { + fn from(cmpop: &Cmpop) -> Self { + match cmpop { + Cmpop::Is => IsCmpop::Is, + Cmpop::IsNot => IsCmpop::IsNot, + _ => unreachable!("Expected Cmpop::Is | Cmpop::IsNot"), + } + } +} + +define_violation!( + pub struct IsLiteral { + pub cmpop: IsCmpop, + } +); +impl AlwaysAutofixableViolation for IsLiteral { + #[derive_message_formats] + fn message(&self) -> String { + let IsLiteral { cmpop } = self; + match cmpop { + IsCmpop::Is => format!("Use `==` to compare constant literals"), + IsCmpop::IsNot => format!("Use `!=` to compare constant literals"), + } + } + + fn autofix_title(&self) -> String { + let IsLiteral { cmpop } = self; + match cmpop { + IsCmpop::Is => "Replace `is` with `==`".to_string(), + IsCmpop::IsNot => "Replace `is not` with `!=`".to_string(), + } + } +} /// F632 pub fn invalid_literal_comparison( @@ -25,8 +67,7 @@ pub fn invalid_literal_comparison( && (helpers::is_constant_non_singleton(left) || helpers::is_constant_non_singleton(right)) { - let mut diagnostic = - Diagnostic::new(violations::IsLiteral { cmpop: op.into() }, location); + let mut diagnostic = Diagnostic::new(IsLiteral { cmpop: op.into() }, location); if checker.patch(diagnostic.kind.rule()) { if let Some(located_op) = &located.get(index) { assert_eq!(&located_op.node, op); diff --git a/src/rules/pyflakes/rules/invalid_print_syntax.rs b/src/rules/pyflakes/rules/invalid_print_syntax.rs index ee9cb990aa..4cfc5c464a 100644 --- a/src/rules/pyflakes/rules/invalid_print_syntax.rs +++ b/src/rules/pyflakes/rules/invalid_print_syntax.rs @@ -1,9 +1,21 @@ +use crate::define_violation; +use ruff_macros::derive_message_formats; use rustpython_ast::{Expr, ExprKind}; use crate::ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::Diagnostic; -use crate::violations; +use crate::violation::Violation; + +define_violation!( + pub struct InvalidPrintSyntax; +); +impl Violation for InvalidPrintSyntax { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use of `>>` is invalid with `print` function") + } +} /// F633 pub fn invalid_print_syntax(checker: &mut Checker, left: &Expr) { @@ -17,7 +29,7 @@ pub fn invalid_print_syntax(checker: &mut Checker, left: &Expr) { return; }; checker.diagnostics.push(Diagnostic::new( - violations::InvalidPrintSyntax, + InvalidPrintSyntax, Range::from_located(left), )); } diff --git a/src/rules/pyflakes/rules/mod.rs b/src/rules/pyflakes/rules/mod.rs index a7e959943d..038508eede 100644 --- a/src/rules/pyflakes/rules/mod.rs +++ b/src/rules/pyflakes/rules/mod.rs @@ -1,187 +1,67 @@ -pub use assert_tuple::assert_tuple; -pub use f_string_missing_placeholders::f_string_missing_placeholders; -pub use if_tuple::if_tuple; -pub use invalid_literal_comparisons::invalid_literal_comparison; -pub use invalid_print_syntax::invalid_print_syntax; -pub use raise_not_implemented::raise_not_implemented; -pub use repeated_keys::repeated_keys; +pub use assert_tuple::{assert_tuple, AssertTuple}; +pub use break_outside_loop::{break_outside_loop, BreakOutsideLoop}; +pub use continue_outside_loop::{continue_outside_loop, ContinueOutsideLoop}; +pub use default_except_not_last::{default_except_not_last, DefaultExceptNotLast}; +pub use f_string_missing_placeholders::{ + f_string_missing_placeholders, FStringMissingPlaceholders, +}; +pub use forward_annotation_syntax_error::ForwardAnnotationSyntaxError; +pub use if_tuple::{if_tuple, IfTuple}; +pub use imports::{ + future_feature_not_defined, FutureFeatureNotDefined, ImportShadowedByLoopVar, + ImportStarNotPermitted, ImportStarUsage, ImportStarUsed, LateFutureImport, UnusedImport, +}; +pub use invalid_literal_comparisons::{invalid_literal_comparison, IsLiteral}; +pub use invalid_print_syntax::{invalid_print_syntax, InvalidPrintSyntax}; +pub use raise_not_implemented::{raise_not_implemented, RaiseNotImplemented}; +pub use redefined_while_unused::RedefinedWhileUnused; +pub use repeated_keys::{ + repeated_keys, MultiValueRepeatedKeyLiteral, MultiValueRepeatedKeyVariable, +}; +pub use return_outside_function::{return_outside_function, ReturnOutsideFunction}; +pub use starred_expressions::{ + starred_expressions, ExpressionsInStarAssignment, TwoStarredExpressions, +}; pub(crate) use strings::{ percent_format_expected_mapping, percent_format_expected_sequence, percent_format_extra_named_arguments, percent_format_missing_arguments, percent_format_mixed_positional_and_named, percent_format_positional_count_mismatch, percent_format_star_requires_sequence, string_dot_format_extra_named_arguments, string_dot_format_extra_positional_arguments, string_dot_format_missing_argument, - string_dot_format_mixing_automatic, + string_dot_format_mixing_automatic, PercentFormatExpectedMapping, + PercentFormatExpectedSequence, PercentFormatExtraNamedArguments, PercentFormatInvalidFormat, + PercentFormatMissingArgument, PercentFormatMixedPositionalAndNamed, + PercentFormatPositionalCountMismatch, PercentFormatStarRequiresSequence, + PercentFormatUnsupportedFormatCharacter, StringDotFormatExtraNamedArguments, + StringDotFormatExtraPositionalArguments, StringDotFormatInvalidFormat, + StringDotFormatMissingArguments, StringDotFormatMixingAutomatic, }; -pub use unused_annotation::unused_annotation; -pub use unused_variable::unused_variable; +pub use undefined_export::UndefinedExport; +pub use undefined_local::{undefined_local, UndefinedLocal}; +pub use undefined_name::UndefinedName; +pub use unused_annotation::{unused_annotation, UnusedAnnotation}; +pub use unused_variable::{unused_variable, UnusedVariable}; +pub use yield_outside_function::{yield_outside_function, YieldOutsideFunction}; mod assert_tuple; +mod break_outside_loop; +mod continue_outside_loop; +mod default_except_not_last; mod f_string_missing_placeholders; +mod forward_annotation_syntax_error; mod if_tuple; +mod imports; mod invalid_literal_comparisons; mod invalid_print_syntax; mod raise_not_implemented; +mod redefined_while_unused; mod repeated_keys; +mod return_outside_function; +mod starred_expressions; mod strings; +mod undefined_export; +mod undefined_local; +mod undefined_name; mod unused_annotation; mod unused_variable; - -use std::string::ToString; - -use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind}; - -use crate::ast::helpers::except_range; -use crate::ast::types::{Binding, Range, Scope, ScopeKind}; -use crate::registry::Diagnostic; -use crate::source_code::Locator; -use crate::violations; - -/// F821 -pub fn undefined_local(name: &str, scopes: &[&Scope], bindings: &[Binding]) -> Option { - let current = &scopes.last().expect("No current scope found"); - if matches!(current.kind, ScopeKind::Function(_)) && !current.values.contains_key(name) { - for scope in scopes.iter().rev().skip(1) { - if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) { - if let Some(binding) = scope.values.get(name).map(|index| &bindings[*index]) { - if let Some((scope_id, location)) = binding.runtime_usage { - if scope_id == current.id { - return Some(Diagnostic::new( - violations::UndefinedLocal { - name: name.to_string(), - }, - location, - )); - } - } - } - } - } - } - None -} - -/// F707 -pub fn default_except_not_last( - handlers: &[Excepthandler], - locator: &Locator, -) -> Option { - for (idx, handler) in handlers.iter().enumerate() { - let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node; - if type_.is_none() && idx < handlers.len() - 1 { - return Some(Diagnostic::new( - violations::DefaultExceptNotLast, - except_range(handler, locator), - )); - } - } - - None -} - -/// F621, F622 -pub fn starred_expressions( - elts: &[Expr], - check_too_many_expressions: bool, - check_two_starred_expressions: bool, - location: Range, -) -> Option { - let mut has_starred: bool = false; - let mut starred_index: Option = None; - for (index, elt) in elts.iter().enumerate() { - if matches!(elt.node, ExprKind::Starred { .. }) { - if has_starred && check_two_starred_expressions { - return Some(Diagnostic::new(violations::TwoStarredExpressions, location)); - } - has_starred = true; - starred_index = Some(index); - } - } - - if check_too_many_expressions { - if let Some(starred_index) = starred_index { - if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 { - return Some(Diagnostic::new( - violations::ExpressionsInStarAssignment, - location, - )); - } - } - } - - None -} - -/// F701 -pub fn break_outside_loop<'a>( - stmt: &'a Stmt, - parents: &mut impl Iterator, -) -> Option { - let mut allowed: bool = false; - let mut child = stmt; - for parent in parents { - match &parent.node { - StmtKind::For { orelse, .. } - | StmtKind::AsyncFor { orelse, .. } - | StmtKind::While { orelse, .. } => { - if !orelse.contains(child) { - allowed = true; - break; - } - } - StmtKind::FunctionDef { .. } - | StmtKind::AsyncFunctionDef { .. } - | StmtKind::ClassDef { .. } => { - break; - } - _ => {} - } - child = parent; - } - - if allowed { - None - } else { - Some(Diagnostic::new( - violations::BreakOutsideLoop, - Range::from_located(stmt), - )) - } -} - -/// F702 -pub fn continue_outside_loop<'a>( - stmt: &'a Stmt, - parents: &mut impl Iterator, -) -> Option { - let mut allowed: bool = false; - let mut child = stmt; - for parent in parents { - match &parent.node { - StmtKind::For { orelse, .. } - | StmtKind::AsyncFor { orelse, .. } - | StmtKind::While { orelse, .. } => { - if !orelse.contains(child) { - allowed = true; - break; - } - } - StmtKind::FunctionDef { .. } - | StmtKind::AsyncFunctionDef { .. } - | StmtKind::ClassDef { .. } => { - break; - } - _ => {} - } - child = parent; - } - - if allowed { - None - } else { - Some(Diagnostic::new( - violations::ContinueOutsideLoop, - Range::from_located(stmt), - )) - } -} +mod yield_outside_function; diff --git a/src/rules/pyflakes/rules/raise_not_implemented.rs b/src/rules/pyflakes/rules/raise_not_implemented.rs index 48f4cb9f0e..564c72f50d 100644 --- a/src/rules/pyflakes/rules/raise_not_implemented.rs +++ b/src/rules/pyflakes/rules/raise_not_implemented.rs @@ -1,10 +1,26 @@ +use crate::define_violation; +use ruff_macros::derive_message_formats; use rustpython_ast::{Expr, ExprKind}; use crate::ast::types::Range; use crate::checkers::ast::Checker; use crate::fix::Fix; use crate::registry::Diagnostic; -use crate::violations; +use crate::violation::AlwaysAutofixableViolation; + +define_violation!( + pub struct RaiseNotImplemented; +); +impl AlwaysAutofixableViolation for RaiseNotImplemented { + #[derive_message_formats] + fn message(&self) -> String { + format!("`raise NotImplemented` should be `raise NotImplementedError`") + } + + fn autofix_title(&self) -> String { + "Use `raise NotImplementedError`".to_string() + } +} fn match_not_implemented(expr: &Expr) -> Option<&Expr> { match &expr.node { @@ -30,8 +46,7 @@ pub fn raise_not_implemented(checker: &mut Checker, expr: &Expr) { let Some(expr) = match_not_implemented(expr) else { return; }; - let mut diagnostic = - Diagnostic::new(violations::RaiseNotImplemented, Range::from_located(expr)); + let mut diagnostic = Diagnostic::new(RaiseNotImplemented, Range::from_located(expr)); if checker.patch(diagnostic.kind.rule()) { diagnostic.amend(Fix::replacement( "NotImplementedError".to_string(), diff --git a/src/rules/pyflakes/rules/redefined_while_unused.rs b/src/rules/pyflakes/rules/redefined_while_unused.rs new file mode 100644 index 0000000000..764653cb71 --- /dev/null +++ b/src/rules/pyflakes/rules/redefined_while_unused.rs @@ -0,0 +1,18 @@ +use crate::define_violation; + +use crate::violation::Violation; +use ruff_macros::derive_message_formats; + +define_violation!( + pub struct RedefinedWhileUnused { + pub name: String, + pub line: usize, + } +); +impl Violation for RedefinedWhileUnused { + #[derive_message_formats] + fn message(&self) -> String { + let RedefinedWhileUnused { name, line } = self; + format!("Redefinition of unused `{name}` from line {line}") + } +} diff --git a/src/rules/pyflakes/rules/repeated_keys.rs b/src/rules/pyflakes/rules/repeated_keys.rs index 8297f31e9f..fc977d583a 100644 --- a/src/rules/pyflakes/rules/repeated_keys.rs +++ b/src/rules/pyflakes/rules/repeated_keys.rs @@ -9,7 +9,63 @@ use crate::ast::types::Range; use crate::checkers::ast::Checker; use crate::fix::Fix; use crate::registry::{Diagnostic, Rule}; -use crate::violations; +use crate::{define_violation, AutofixKind}; + +use crate::violation::{Availability, Violation}; +use ruff_macros::derive_message_formats; + +define_violation!( + pub struct MultiValueRepeatedKeyLiteral { + pub name: String, + pub repeated_value: bool, + } +); +impl Violation for MultiValueRepeatedKeyLiteral { + const AUTOFIX: Option = Some(AutofixKind::new(Availability::Always)); + + #[derive_message_formats] + fn message(&self) -> String { + let MultiValueRepeatedKeyLiteral { name, .. } = self; + format!("Dictionary key literal `{name}` repeated") + } + + fn autofix_title_formatter(&self) -> Option String> { + let MultiValueRepeatedKeyLiteral { repeated_value, .. } = self; + if *repeated_value { + Some(|MultiValueRepeatedKeyLiteral { name, .. }| { + format!("Remove repeated key literal `{name}`") + }) + } else { + None + } + } +} +define_violation!( + pub struct MultiValueRepeatedKeyVariable { + pub name: String, + pub repeated_value: bool, + } +); +impl Violation for MultiValueRepeatedKeyVariable { + const AUTOFIX: Option = Some(AutofixKind::new(Availability::Always)); + + #[derive_message_formats] + fn message(&self) -> String { + let MultiValueRepeatedKeyVariable { name, .. } = self; + format!("Dictionary key `{name}` repeated") + } + + fn autofix_title_formatter(&self) -> Option String> { + let MultiValueRepeatedKeyVariable { repeated_value, .. } = self; + if *repeated_value { + Some(|MultiValueRepeatedKeyVariable { name, .. }| { + format!("Remove repeated key `{name}`") + }) + } else { + None + } + } +} #[derive(Debug, Eq, PartialEq, Hash)] enum DictionaryKey<'a> { @@ -48,7 +104,7 @@ pub fn repeated_keys(checker: &mut Checker, keys: &[Option], values: &[Exp let comparable_value: ComparableExpr = (&values[i]).into(); let is_duplicate_value = seen_values.contains(&comparable_value); let mut diagnostic = Diagnostic::new( - violations::MultiValueRepeatedKeyLiteral { + MultiValueRepeatedKeyLiteral { name: unparse_expr(key, checker.stylist), repeated_value: is_duplicate_value, }, @@ -76,7 +132,7 @@ pub fn repeated_keys(checker: &mut Checker, keys: &[Option], values: &[Exp let comparable_value: ComparableExpr = (&values[i]).into(); let is_duplicate_value = seen_values.contains(&comparable_value); let mut diagnostic = Diagnostic::new( - violations::MultiValueRepeatedKeyVariable { + MultiValueRepeatedKeyVariable { name: dict_key.to_string(), repeated_value: is_duplicate_value, }, diff --git a/src/rules/pyflakes/rules/return_outside_function.rs b/src/rules/pyflakes/rules/return_outside_function.rs new file mode 100644 index 0000000000..3493fec93f --- /dev/null +++ b/src/rules/pyflakes/rules/return_outside_function.rs @@ -0,0 +1,31 @@ +use crate::ast::types::{Range, ScopeKind}; +use crate::checkers::ast::Checker; +use crate::define_violation; +use crate::registry::Diagnostic; +use crate::violation::Violation; +use ruff_macros::derive_message_formats; +use rustpython_ast::Stmt; + +define_violation!( + pub struct ReturnOutsideFunction; +); +impl Violation for ReturnOutsideFunction { + #[derive_message_formats] + fn message(&self) -> String { + format!("`return` statement outside of a function/method") + } +} + +pub fn return_outside_function(checker: &mut Checker, stmt: &Stmt) { + if let Some(&index) = checker.scope_stack.last() { + if matches!( + checker.scopes[index].kind, + ScopeKind::Class(_) | ScopeKind::Module + ) { + checker.diagnostics.push(Diagnostic::new( + ReturnOutsideFunction, + Range::from_located(stmt), + )); + } + } +} diff --git a/src/rules/pyflakes/rules/starred_expressions.rs b/src/rules/pyflakes/rules/starred_expressions.rs new file mode 100644 index 0000000000..c07e363ebc --- /dev/null +++ b/src/rules/pyflakes/rules/starred_expressions.rs @@ -0,0 +1,56 @@ +use crate::ast::types::Range; +use crate::define_violation; +use crate::registry::Diagnostic; +use crate::violation::Violation; +use ruff_macros::derive_message_formats; +use rustpython_parser::ast::{Expr, ExprKind}; + +define_violation!( + pub struct ExpressionsInStarAssignment; +); +impl Violation for ExpressionsInStarAssignment { + #[derive_message_formats] + fn message(&self) -> String { + format!("Too many expressions in star-unpacking assignment") + } +} + +define_violation!( + pub struct TwoStarredExpressions; +); +impl Violation for TwoStarredExpressions { + #[derive_message_formats] + fn message(&self) -> String { + format!("Two starred expressions in assignment") + } +} + +/// F621, F622 +pub fn starred_expressions( + elts: &[Expr], + check_too_many_expressions: bool, + check_two_starred_expressions: bool, + location: Range, +) -> Option { + let mut has_starred: bool = false; + let mut starred_index: Option = None; + for (index, elt) in elts.iter().enumerate() { + if matches!(elt.node, ExprKind::Starred { .. }) { + if has_starred && check_two_starred_expressions { + return Some(Diagnostic::new(TwoStarredExpressions, location)); + } + has_starred = true; + starred_index = Some(index); + } + } + + if check_too_many_expressions { + if let Some(starred_index) = starred_index { + if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 { + return Some(Diagnostic::new(ExpressionsInStarAssignment, location)); + } + } + } + + None +} diff --git a/src/rules/pyflakes/rules/strings.rs b/src/rules/pyflakes/rules/strings.rs index d030b8487c..2962f21372 100644 --- a/src/rules/pyflakes/rules/strings.rs +++ b/src/rules/pyflakes/rules/strings.rs @@ -1,3 +1,5 @@ +use crate::define_violation; +use ruff_macros::derive_message_formats; use std::string::ToString; use log::error; @@ -13,7 +15,192 @@ use super::super::format::FormatSummary; use crate::ast::types::Range; use crate::checkers::ast::Checker; use crate::registry::Diagnostic; -use crate::violations; +use crate::violation::{AlwaysAutofixableViolation, Violation}; + +define_violation!( + pub struct PercentFormatInvalidFormat { + pub message: String, + } +); +impl Violation for PercentFormatInvalidFormat { + #[derive_message_formats] + fn message(&self) -> String { + let PercentFormatInvalidFormat { message } = self; + format!("`%`-format string has invalid format string: {message}") + } +} + +define_violation!( + pub struct PercentFormatExpectedMapping; +); +impl Violation for PercentFormatExpectedMapping { + #[derive_message_formats] + fn message(&self) -> String { + format!("`%`-format string expected mapping but got sequence") + } +} + +define_violation!( + pub struct PercentFormatExpectedSequence; +); +impl Violation for PercentFormatExpectedSequence { + #[derive_message_formats] + fn message(&self) -> String { + format!("`%`-format string expected sequence but got mapping") + } +} + +define_violation!( + pub struct PercentFormatExtraNamedArguments { + pub missing: Vec, + } +); +impl AlwaysAutofixableViolation for PercentFormatExtraNamedArguments { + #[derive_message_formats] + fn message(&self) -> String { + let PercentFormatExtraNamedArguments { missing } = self; + let message = missing.join(", "); + format!("`%`-format string has unused named argument(s): {message}") + } + + fn autofix_title(&self) -> String { + let PercentFormatExtraNamedArguments { missing } = self; + let message = missing.join(", "); + format!("Remove extra named arguments: {message}") + } +} + +define_violation!( + pub struct PercentFormatMissingArgument { + pub missing: Vec, + } +); +impl Violation for PercentFormatMissingArgument { + #[derive_message_formats] + fn message(&self) -> String { + let PercentFormatMissingArgument { missing } = self; + let message = missing.join(", "); + format!("`%`-format string is missing argument(s) for placeholder(s): {message}") + } +} + +define_violation!( + pub struct PercentFormatMixedPositionalAndNamed; +); +impl Violation for PercentFormatMixedPositionalAndNamed { + #[derive_message_formats] + fn message(&self) -> String { + format!("`%`-format string has mixed positional and named placeholders") + } +} + +define_violation!( + pub struct PercentFormatPositionalCountMismatch { + pub wanted: usize, + pub got: usize, + } +); +impl Violation for PercentFormatPositionalCountMismatch { + #[derive_message_formats] + fn message(&self) -> String { + let PercentFormatPositionalCountMismatch { wanted, got } = self; + format!("`%`-format string has {wanted} placeholder(s) but {got} substitution(s)") + } +} + +define_violation!( + pub struct PercentFormatStarRequiresSequence; +); +impl Violation for PercentFormatStarRequiresSequence { + #[derive_message_formats] + fn message(&self) -> String { + format!("`%`-format string `*` specifier requires sequence") + } +} + +define_violation!( + pub struct PercentFormatUnsupportedFormatCharacter { + pub char: char, + } +); +impl Violation for PercentFormatUnsupportedFormatCharacter { + #[derive_message_formats] + fn message(&self) -> String { + let PercentFormatUnsupportedFormatCharacter { char } = self; + format!("`%`-format string has unsupported format character '{char}'") + } +} + +define_violation!( + pub struct StringDotFormatInvalidFormat { + pub message: String, + } +); +impl Violation for StringDotFormatInvalidFormat { + #[derive_message_formats] + fn message(&self) -> String { + let StringDotFormatInvalidFormat { message } = self; + format!("`.format` call has invalid format string: {message}") + } +} + +define_violation!( + pub struct StringDotFormatExtraNamedArguments { + pub missing: Vec, + } +); +impl AlwaysAutofixableViolation for StringDotFormatExtraNamedArguments { + #[derive_message_formats] + fn message(&self) -> String { + let StringDotFormatExtraNamedArguments { missing } = self; + let message = missing.join(", "); + format!("`.format` call has unused named argument(s): {message}") + } + + fn autofix_title(&self) -> String { + let StringDotFormatExtraNamedArguments { missing } = self; + let message = missing.join(", "); + format!("Remove extra named arguments: {message}") + } +} + +define_violation!( + pub struct StringDotFormatExtraPositionalArguments { + pub missing: Vec, + } +); +impl Violation for StringDotFormatExtraPositionalArguments { + #[derive_message_formats] + fn message(&self) -> String { + let StringDotFormatExtraPositionalArguments { missing } = self; + let message = missing.join(", "); + format!("`.format` call has unused arguments at position(s): {message}") + } +} + +define_violation!( + pub struct StringDotFormatMissingArguments { + pub missing: Vec, + } +); +impl Violation for StringDotFormatMissingArguments { + #[derive_message_formats] + fn message(&self) -> String { + let StringDotFormatMissingArguments { missing } = self; + let message = missing.join(", "); + format!("`.format` call is missing argument(s) for placeholder(s): {message}") + } +} + +define_violation!( + pub struct StringDotFormatMixingAutomatic; +); +impl Violation for StringDotFormatMixingAutomatic { + #[derive_message_formats] + fn message(&self) -> String { + format!("`.format` string mixes automatic and manual numbering") + } +} fn has_star_star_kwargs(keywords: &[Keyword]) -> bool { keywords.iter().any(|k| { @@ -42,10 +229,9 @@ pub(crate) fn percent_format_expected_mapping( | ExprKind::Set { .. } | ExprKind::ListComp { .. } | ExprKind::SetComp { .. } - | ExprKind::GeneratorExp { .. } => checker.diagnostics.push(Diagnostic::new( - violations::PercentFormatExpectedMapping, - location, - )), + | ExprKind::GeneratorExp { .. } => checker + .diagnostics + .push(Diagnostic::new(PercentFormatExpectedMapping, location)), _ => {} } } @@ -64,10 +250,9 @@ pub(crate) fn percent_format_expected_sequence( ExprKind::Dict { .. } | ExprKind::DictComp { .. } ) { - checker.diagnostics.push(Diagnostic::new( - violations::PercentFormatExpectedSequence, - location, - )); + checker + .diagnostics + .push(Diagnostic::new(PercentFormatExpectedSequence, location)); } } @@ -114,7 +299,7 @@ pub(crate) fn percent_format_extra_named_arguments( } let mut diagnostic = Diagnostic::new( - violations::PercentFormatExtraNamedArguments { + PercentFormatExtraNamedArguments { missing: missing.iter().map(|&arg| arg.to_string()).collect(), }, location, @@ -174,7 +359,7 @@ pub(crate) fn percent_format_missing_arguments( if !missing.is_empty() { checker.diagnostics.push(Diagnostic::new( - violations::PercentFormatMissingArgument { + PercentFormatMissingArgument { missing: missing.iter().map(|&s| s.clone()).collect(), }, location, @@ -191,7 +376,7 @@ pub(crate) fn percent_format_mixed_positional_and_named( ) { if !(summary.num_positional == 0 || summary.keywords.is_empty()) { checker.diagnostics.push(Diagnostic::new( - violations::PercentFormatMixedPositionalAndNamed, + PercentFormatMixedPositionalAndNamed, location, )); } @@ -220,7 +405,7 @@ pub(crate) fn percent_format_positional_count_mismatch( if found != summary.num_positional { checker.diagnostics.push(Diagnostic::new( - violations::PercentFormatPositionalCountMismatch { + PercentFormatPositionalCountMismatch { wanted: summary.num_positional, got: found, }, @@ -241,9 +426,9 @@ pub(crate) fn percent_format_star_requires_sequence( ) { if summary.starred { match &right.node { - ExprKind::Dict { .. } | ExprKind::DictComp { .. } => checker.diagnostics.push( - Diagnostic::new(violations::PercentFormatStarRequiresSequence, location), - ), + ExprKind::Dict { .. } | ExprKind::DictComp { .. } => checker + .diagnostics + .push(Diagnostic::new(PercentFormatStarRequiresSequence, location)), _ => {} } } @@ -280,7 +465,7 @@ pub(crate) fn string_dot_format_extra_named_arguments( } let mut diagnostic = Diagnostic::new( - violations::StringDotFormatExtraNamedArguments { + StringDotFormatExtraNamedArguments { missing: missing.iter().map(|&arg| arg.to_string()).collect(), }, location, @@ -321,7 +506,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments( } checker.diagnostics.push(Diagnostic::new( - violations::StringDotFormatExtraPositionalArguments { + StringDotFormatExtraPositionalArguments { missing: missing .iter() .map(std::string::ToString::to_string) @@ -368,7 +553,7 @@ pub(crate) fn string_dot_format_missing_argument( if !missing.is_empty() { checker.diagnostics.push(Diagnostic::new( - violations::StringDotFormatMissingArguments { missing }, + StringDotFormatMissingArguments { missing }, location, )); } @@ -381,9 +566,8 @@ pub(crate) fn string_dot_format_mixing_automatic( location: Range, ) { if !(summary.autos.is_empty() || summary.indexes.is_empty()) { - checker.diagnostics.push(Diagnostic::new( - violations::StringDotFormatMixingAutomatic, - location, - )); + checker + .diagnostics + .push(Diagnostic::new(StringDotFormatMixingAutomatic, location)); } } diff --git a/src/rules/pyflakes/rules/undefined_export.rs b/src/rules/pyflakes/rules/undefined_export.rs new file mode 100644 index 0000000000..43cf5e606c --- /dev/null +++ b/src/rules/pyflakes/rules/undefined_export.rs @@ -0,0 +1,17 @@ +use crate::define_violation; + +use crate::violation::Violation; +use ruff_macros::derive_message_formats; + +define_violation!( + pub struct UndefinedExport { + pub name: String, + } +); +impl Violation for UndefinedExport { + #[derive_message_formats] + fn message(&self) -> String { + let UndefinedExport { name } = self; + format!("Undefined name `{name}` in `__all__`") + } +} diff --git a/src/rules/pyflakes/rules/undefined_local.rs b/src/rules/pyflakes/rules/undefined_local.rs new file mode 100644 index 0000000000..ba4168f058 --- /dev/null +++ b/src/rules/pyflakes/rules/undefined_local.rs @@ -0,0 +1,46 @@ +use crate::ast::types::{Binding, Scope, ScopeKind}; +use crate::registry::Diagnostic; + +use crate::define_violation; + +use ruff_macros::derive_message_formats; + +use crate::violation::Violation; +use std::string::ToString; + +define_violation!( + pub struct UndefinedLocal { + pub name: String, + } +); +impl Violation for UndefinedLocal { + #[derive_message_formats] + fn message(&self) -> String { + let UndefinedLocal { name } = self; + format!("Local variable `{name}` referenced before assignment") + } +} + +/// F821 +pub fn undefined_local(name: &str, scopes: &[&Scope], bindings: &[Binding]) -> Option { + let current = &scopes.last().expect("No current scope found"); + if matches!(current.kind, ScopeKind::Function(_)) && !current.values.contains_key(name) { + for scope in scopes.iter().rev().skip(1) { + if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) { + if let Some(binding) = scope.values.get(name).map(|index| &bindings[*index]) { + if let Some((scope_id, location)) = binding.runtime_usage { + if scope_id == current.id { + return Some(Diagnostic::new( + UndefinedLocal { + name: name.to_string(), + }, + location, + )); + } + } + } + } + } + } + None +} diff --git a/src/rules/pyflakes/rules/undefined_name.rs b/src/rules/pyflakes/rules/undefined_name.rs new file mode 100644 index 0000000000..75f4b3d06c --- /dev/null +++ b/src/rules/pyflakes/rules/undefined_name.rs @@ -0,0 +1,16 @@ +use crate::define_violation; +use crate::violation::Violation; +use ruff_macros::derive_message_formats; + +define_violation!( + pub struct UndefinedName { + pub name: String, + } +); +impl Violation for UndefinedName { + #[derive_message_formats] + fn message(&self) -> String { + let UndefinedName { name } = self; + format!("Undefined name `{name}`") + } +} diff --git a/src/rules/pyflakes/rules/unused_annotation.rs b/src/rules/pyflakes/rules/unused_annotation.rs index 65d55af980..a60ac3c332 100644 --- a/src/rules/pyflakes/rules/unused_annotation.rs +++ b/src/rules/pyflakes/rules/unused_annotation.rs @@ -1,7 +1,23 @@ use crate::ast::types::BindingKind; use crate::checkers::ast::Checker; +use crate::define_violation; use crate::registry::Diagnostic; -use crate::violations; + +use crate::violation::Violation; +use ruff_macros::derive_message_formats; + +define_violation!( + pub struct UnusedAnnotation { + pub name: String, + } +); +impl Violation for UnusedAnnotation { + #[derive_message_formats] + fn message(&self) -> String { + let UnusedAnnotation { name } = self; + format!("Local variable `{name}` is annotated but never used") + } +} /// F842 pub fn unused_annotation(checker: &mut Checker, scope: usize) { @@ -16,7 +32,7 @@ pub fn unused_annotation(checker: &mut Checker, scope: usize) { && !checker.settings.dummy_variable_rgx.is_match(name) { checker.diagnostics.push(Diagnostic::new( - violations::UnusedAnnotation { + UnusedAnnotation { name: (*name).to_string(), }, binding.range, diff --git a/src/rules/pyflakes/rules/unused_variable.rs b/src/rules/pyflakes/rules/unused_variable.rs index f008b8795b..3fdd7ec100 100644 --- a/src/rules/pyflakes/rules/unused_variable.rs +++ b/src/rules/pyflakes/rules/unused_variable.rs @@ -1,5 +1,7 @@ +use crate::define_violation; use itertools::Itertools; use log::error; +use ruff_macros::derive_message_formats; use rustpython_ast::{ExprKind, Location, Stmt, StmtKind}; use rustpython_parser::lexer; use rustpython_parser::lexer::Tok; @@ -11,7 +13,25 @@ use crate::checkers::ast::Checker; use crate::fix::Fix; use crate::registry::Diagnostic; use crate::source_code::Locator; -use crate::violations; +use crate::violation::AlwaysAutofixableViolation; + +define_violation!( + pub struct UnusedVariable { + pub name: String, + } +); +impl AlwaysAutofixableViolation for UnusedVariable { + #[derive_message_formats] + fn message(&self) -> String { + let UnusedVariable { name } = self; + format!("Local variable `{name}` is assigned to but never used") + } + + fn autofix_title(&self) -> String { + let UnusedVariable { name } = self; + format!("Remove assignment to unused variable `{name}`") + } +} fn match_token_after(stmt: &Stmt, locator: &Locator, f: F) -> Location where @@ -174,7 +194,7 @@ pub fn unused_variable(checker: &mut Checker, scope: usize) { && name != &"__traceback_supplement__" { let mut diagnostic = Diagnostic::new( - violations::UnusedVariable { + UnusedVariable { name: (*name).to_string(), }, binding.range, diff --git a/src/rules/pyflakes/rules/yield_outside_function.rs b/src/rules/pyflakes/rules/yield_outside_function.rs new file mode 100644 index 0000000000..88936057d4 --- /dev/null +++ b/src/rules/pyflakes/rules/yield_outside_function.rs @@ -0,0 +1,57 @@ +use crate::ast::types::{Range, ScopeKind}; +use crate::checkers::ast::Checker; +use crate::define_violation; +use crate::registry::Diagnostic; +use crate::violation::Violation; +use ruff_macros::derive_message_formats; +use rustpython_ast::{Expr, ExprKind}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum DeferralKeyword { + Yield, + YieldFrom, + Await, +} + +impl fmt::Display for DeferralKeyword { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + DeferralKeyword::Yield => fmt.write_str("yield"), + DeferralKeyword::YieldFrom => fmt.write_str("yield from"), + DeferralKeyword::Await => fmt.write_str("await"), + } + } +} + +define_violation!( + pub struct YieldOutsideFunction { + pub keyword: DeferralKeyword, + } +); +impl Violation for YieldOutsideFunction { + #[derive_message_formats] + fn message(&self) -> String { + let YieldOutsideFunction { keyword } = self; + format!("`{keyword}` statement outside of a function") + } +} + +pub fn yield_outside_function(checker: &mut Checker, expr: &Expr) { + if matches!( + checker.current_scope().kind, + ScopeKind::Class(_) | ScopeKind::Module + ) { + let keyword = match expr.node { + ExprKind::Yield { .. } => DeferralKeyword::Yield, + ExprKind::YieldFrom { .. } => DeferralKeyword::YieldFrom, + ExprKind::Await { .. } => DeferralKeyword::Await, + _ => unreachable!("Expected ExprKind::Yield | ExprKind::YieldFrom | ExprKind::Await"), + }; + checker.diagnostics.push(Diagnostic::new( + YieldOutsideFunction { keyword }, + Range::from_located(expr), + )); + } +} diff --git a/src/violations.rs b/src/violations.rs index 180c5377ac..52ed9c2753 100644 --- a/src/violations.rs +++ b/src/violations.rs @@ -59,656 +59,6 @@ impl Violation for SyntaxError { } } -// pyflakes - -define_violation!( - pub struct UnusedImport { - pub name: String, - pub ignore_init: bool, - pub multiple: bool, - } -); -fn fmt_unused_import_autofix_msg(unused_import: &UnusedImport) -> String { - let UnusedImport { name, multiple, .. } = unused_import; - if *multiple { - "Remove unused import".to_string() - } else { - format!("Remove unused import: `{name}`") - } -} -impl Violation for UnusedImport { - const AUTOFIX: Option = Some(AutofixKind::new(Availability::Always)); - - #[derive_message_formats] - fn message(&self) -> String { - let UnusedImport { - name, ignore_init, .. - } = self; - if *ignore_init { - format!( - "`{name}` imported but unused; consider adding to `__all__` or using a redundant \ - alias" - ) - } else { - format!("`{name}` imported but unused") - } - } - - fn autofix_title_formatter(&self) -> Option String> { - let UnusedImport { ignore_init, .. } = self; - if *ignore_init { - None - } else { - Some(fmt_unused_import_autofix_msg) - } - } -} - -define_violation!( - pub struct ImportShadowedByLoopVar { - pub name: String, - pub line: usize, - } -); -impl Violation for ImportShadowedByLoopVar { - #[derive_message_formats] - fn message(&self) -> String { - let ImportShadowedByLoopVar { name, line } = self; - format!("Import `{name}` from line {line} shadowed by loop variable") - } -} - -define_violation!( - pub struct ImportStarUsed { - pub name: String, - } -); -impl Violation for ImportStarUsed { - #[derive_message_formats] - fn message(&self) -> String { - let ImportStarUsed { name } = self; - format!("`from {name} import *` used; unable to detect undefined names") - } -} - -define_violation!( - pub struct LateFutureImport; -); -impl Violation for LateFutureImport { - #[derive_message_formats] - fn message(&self) -> String { - format!("`from __future__` imports must occur at the beginning of the file") - } -} - -define_violation!( - pub struct ImportStarUsage { - pub name: String, - pub sources: Vec, - } -); -impl Violation for ImportStarUsage { - #[derive_message_formats] - fn message(&self) -> String { - let ImportStarUsage { name, sources } = self; - let sources = sources - .iter() - .map(|source| format!("`{source}`")) - .join(", "); - format!("`{name}` may be undefined, or defined from star imports: {sources}") - } -} - -define_violation!( - pub struct ImportStarNotPermitted { - pub name: String, - } -); -impl Violation for ImportStarNotPermitted { - #[derive_message_formats] - fn message(&self) -> String { - let ImportStarNotPermitted { name } = self; - format!("`from {name} import *` only allowed at module level") - } -} - -define_violation!( - pub struct FutureFeatureNotDefined { - pub name: String, - } -); -impl Violation for FutureFeatureNotDefined { - #[derive_message_formats] - fn message(&self) -> String { - let FutureFeatureNotDefined { name } = self; - format!("Future feature `{name}` is not defined") - } -} - -define_violation!( - pub struct PercentFormatInvalidFormat { - pub message: String, - } -); -impl Violation for PercentFormatInvalidFormat { - #[derive_message_formats] - fn message(&self) -> String { - let PercentFormatInvalidFormat { message } = self; - format!("`%`-format string has invalid format string: {message}") - } -} - -define_violation!( - pub struct PercentFormatExpectedMapping; -); -impl Violation for PercentFormatExpectedMapping { - #[derive_message_formats] - fn message(&self) -> String { - format!("`%`-format string expected mapping but got sequence") - } -} - -define_violation!( - pub struct PercentFormatExpectedSequence; -); -impl Violation for PercentFormatExpectedSequence { - #[derive_message_formats] - fn message(&self) -> String { - format!("`%`-format string expected sequence but got mapping") - } -} - -define_violation!( - pub struct PercentFormatExtraNamedArguments { - pub missing: Vec, - } -); -impl AlwaysAutofixableViolation for PercentFormatExtraNamedArguments { - #[derive_message_formats] - fn message(&self) -> String { - let PercentFormatExtraNamedArguments { missing } = self; - let message = missing.join(", "); - format!("`%`-format string has unused named argument(s): {message}") - } - - fn autofix_title(&self) -> String { - let PercentFormatExtraNamedArguments { missing } = self; - let message = missing.join(", "); - format!("Remove extra named arguments: {message}") - } -} - -define_violation!( - pub struct PercentFormatMissingArgument { - pub missing: Vec, - } -); -impl Violation for PercentFormatMissingArgument { - #[derive_message_formats] - fn message(&self) -> String { - let PercentFormatMissingArgument { missing } = self; - let message = missing.join(", "); - format!("`%`-format string is missing argument(s) for placeholder(s): {message}") - } -} - -define_violation!( - pub struct PercentFormatMixedPositionalAndNamed; -); -impl Violation for PercentFormatMixedPositionalAndNamed { - #[derive_message_formats] - fn message(&self) -> String { - format!("`%`-format string has mixed positional and named placeholders") - } -} - -define_violation!( - pub struct PercentFormatPositionalCountMismatch { - pub wanted: usize, - pub got: usize, - } -); -impl Violation for PercentFormatPositionalCountMismatch { - #[derive_message_formats] - fn message(&self) -> String { - let PercentFormatPositionalCountMismatch { wanted, got } = self; - format!("`%`-format string has {wanted} placeholder(s) but {got} substitution(s)") - } -} - -define_violation!( - pub struct PercentFormatStarRequiresSequence; -); -impl Violation for PercentFormatStarRequiresSequence { - #[derive_message_formats] - fn message(&self) -> String { - format!("`%`-format string `*` specifier requires sequence") - } -} - -define_violation!( - pub struct PercentFormatUnsupportedFormatCharacter { - pub char: char, - } -); -impl Violation for PercentFormatUnsupportedFormatCharacter { - #[derive_message_formats] - fn message(&self) -> String { - let PercentFormatUnsupportedFormatCharacter { char } = self; - format!("`%`-format string has unsupported format character '{char}'") - } -} - -define_violation!( - pub struct StringDotFormatInvalidFormat { - pub message: String, - } -); -impl Violation for StringDotFormatInvalidFormat { - #[derive_message_formats] - fn message(&self) -> String { - let StringDotFormatInvalidFormat { message } = self; - format!("`.format` call has invalid format string: {message}") - } -} - -define_violation!( - pub struct StringDotFormatExtraNamedArguments { - pub missing: Vec, - } -); -impl AlwaysAutofixableViolation for StringDotFormatExtraNamedArguments { - #[derive_message_formats] - fn message(&self) -> String { - let StringDotFormatExtraNamedArguments { missing } = self; - let message = missing.join(", "); - format!("`.format` call has unused named argument(s): {message}") - } - - fn autofix_title(&self) -> String { - let StringDotFormatExtraNamedArguments { missing } = self; - let message = missing.join(", "); - format!("Remove extra named arguments: {message}") - } -} - -define_violation!( - pub struct StringDotFormatExtraPositionalArguments { - pub missing: Vec, - } -); -impl Violation for StringDotFormatExtraPositionalArguments { - #[derive_message_formats] - fn message(&self) -> String { - let StringDotFormatExtraPositionalArguments { missing } = self; - let message = missing.join(", "); - format!("`.format` call has unused arguments at position(s): {message}") - } -} - -define_violation!( - pub struct StringDotFormatMissingArguments { - pub missing: Vec, - } -); -impl Violation for StringDotFormatMissingArguments { - #[derive_message_formats] - fn message(&self) -> String { - let StringDotFormatMissingArguments { missing } = self; - let message = missing.join(", "); - format!("`.format` call is missing argument(s) for placeholder(s): {message}") - } -} - -define_violation!( - pub struct StringDotFormatMixingAutomatic; -); -impl Violation for StringDotFormatMixingAutomatic { - #[derive_message_formats] - fn message(&self) -> String { - format!("`.format` string mixes automatic and manual numbering") - } -} - -define_violation!( - pub struct FStringMissingPlaceholders; -); -impl AlwaysAutofixableViolation for FStringMissingPlaceholders { - #[derive_message_formats] - fn message(&self) -> String { - format!("f-string without any placeholders") - } - - fn autofix_title(&self) -> String { - "Remove extraneous `f` prefix".to_string() - } -} - -define_violation!( - pub struct MultiValueRepeatedKeyLiteral { - pub name: String, - pub repeated_value: bool, - } -); -impl Violation for MultiValueRepeatedKeyLiteral { - const AUTOFIX: Option = Some(AutofixKind::new(Availability::Always)); - - #[derive_message_formats] - fn message(&self) -> String { - let MultiValueRepeatedKeyLiteral { name, .. } = self; - format!("Dictionary key literal `{name}` repeated") - } - - fn autofix_title_formatter(&self) -> Option String> { - let MultiValueRepeatedKeyLiteral { repeated_value, .. } = self; - if *repeated_value { - Some(|MultiValueRepeatedKeyLiteral { name, .. }| { - format!("Remove repeated key literal `{name}`") - }) - } else { - None - } - } -} - -define_violation!( - pub struct MultiValueRepeatedKeyVariable { - pub name: String, - pub repeated_value: bool, - } -); -impl Violation for MultiValueRepeatedKeyVariable { - const AUTOFIX: Option = Some(AutofixKind::new(Availability::Always)); - - #[derive_message_formats] - fn message(&self) -> String { - let MultiValueRepeatedKeyVariable { name, .. } = self; - format!("Dictionary key `{name}` repeated") - } - - fn autofix_title_formatter(&self) -> Option String> { - let MultiValueRepeatedKeyVariable { repeated_value, .. } = self; - if *repeated_value { - Some(|MultiValueRepeatedKeyVariable { name, .. }| { - format!("Remove repeated key `{name}`") - }) - } else { - None - } - } -} - -define_violation!( - pub struct ExpressionsInStarAssignment; -); -impl Violation for ExpressionsInStarAssignment { - #[derive_message_formats] - fn message(&self) -> String { - format!("Too many expressions in star-unpacking assignment") - } -} - -define_violation!( - pub struct TwoStarredExpressions; -); -impl Violation for TwoStarredExpressions { - #[derive_message_formats] - fn message(&self) -> String { - format!("Two starred expressions in assignment") - } -} - -define_violation!( - pub struct AssertTuple; -); -impl Violation for AssertTuple { - #[derive_message_formats] - fn message(&self) -> String { - format!("Assert test is a non-empty tuple, which is always `True`") - } -} - -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum IsCmpop { - Is, - IsNot, -} - -impl From<&Cmpop> for IsCmpop { - fn from(cmpop: &Cmpop) -> Self { - match cmpop { - Cmpop::Is => IsCmpop::Is, - Cmpop::IsNot => IsCmpop::IsNot, - _ => unreachable!("Expected Cmpop::Is | Cmpop::IsNot"), - } - } -} - -define_violation!( - pub struct IsLiteral { - pub cmpop: IsCmpop, - } -); -impl AlwaysAutofixableViolation for IsLiteral { - #[derive_message_formats] - fn message(&self) -> String { - let IsLiteral { cmpop } = self; - match cmpop { - IsCmpop::Is => format!("Use `==` to compare constant literals"), - IsCmpop::IsNot => format!("Use `!=` to compare constant literals"), - } - } - - fn autofix_title(&self) -> String { - let IsLiteral { cmpop } = self; - match cmpop { - IsCmpop::Is => "Replace `is` with `==`".to_string(), - IsCmpop::IsNot => "Replace `is not` with `!=`".to_string(), - } - } -} - -define_violation!( - pub struct InvalidPrintSyntax; -); -impl Violation for InvalidPrintSyntax { - #[derive_message_formats] - fn message(&self) -> String { - format!("Use of `>>` is invalid with `print` function") - } -} - -define_violation!( - pub struct IfTuple; -); -impl Violation for IfTuple { - #[derive_message_formats] - fn message(&self) -> String { - format!("If test is a tuple, which is always `True`") - } -} - -define_violation!( - pub struct BreakOutsideLoop; -); -impl Violation for BreakOutsideLoop { - #[derive_message_formats] - fn message(&self) -> String { - format!("`break` outside loop") - } -} - -define_violation!( - pub struct ContinueOutsideLoop; -); -impl Violation for ContinueOutsideLoop { - #[derive_message_formats] - fn message(&self) -> String { - format!("`continue` not properly in loop") - } -} - -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum DeferralKeyword { - Yield, - YieldFrom, - Await, -} - -impl fmt::Display for DeferralKeyword { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - DeferralKeyword::Yield => fmt.write_str("yield"), - DeferralKeyword::YieldFrom => fmt.write_str("yield from"), - DeferralKeyword::Await => fmt.write_str("await"), - } - } -} - -define_violation!( - pub struct YieldOutsideFunction { - pub keyword: DeferralKeyword, - } -); -impl Violation for YieldOutsideFunction { - #[derive_message_formats] - fn message(&self) -> String { - let YieldOutsideFunction { keyword } = self; - format!("`{keyword}` statement outside of a function") - } -} - -define_violation!( - pub struct ReturnOutsideFunction; -); -impl Violation for ReturnOutsideFunction { - #[derive_message_formats] - fn message(&self) -> String { - format!("`return` statement outside of a function/method") - } -} - -define_violation!( - pub struct DefaultExceptNotLast; -); -impl Violation for DefaultExceptNotLast { - #[derive_message_formats] - fn message(&self) -> String { - format!("An `except` block as not the last exception handler") - } -} - -define_violation!( - pub struct ForwardAnnotationSyntaxError { - pub body: String, - } -); -impl Violation for ForwardAnnotationSyntaxError { - #[derive_message_formats] - fn message(&self) -> String { - let ForwardAnnotationSyntaxError { body } = self; - format!("Syntax error in forward annotation: `{body}`") - } -} - -define_violation!( - pub struct RedefinedWhileUnused { - pub name: String, - pub line: usize, - } -); -impl Violation for RedefinedWhileUnused { - #[derive_message_formats] - fn message(&self) -> String { - let RedefinedWhileUnused { name, line } = self; - format!("Redefinition of unused `{name}` from line {line}") - } -} - -define_violation!( - pub struct UndefinedName { - pub name: String, - } -); -impl Violation for UndefinedName { - #[derive_message_formats] - fn message(&self) -> String { - let UndefinedName { name } = self; - format!("Undefined name `{name}`") - } -} - -define_violation!( - pub struct UndefinedExport { - pub name: String, - } -); -impl Violation for UndefinedExport { - #[derive_message_formats] - fn message(&self) -> String { - let UndefinedExport { name } = self; - format!("Undefined name `{name}` in `__all__`") - } -} - -define_violation!( - pub struct UndefinedLocal { - pub name: String, - } -); -impl Violation for UndefinedLocal { - #[derive_message_formats] - fn message(&self) -> String { - let UndefinedLocal { name } = self; - format!("Local variable `{name}` referenced before assignment") - } -} - -define_violation!( - pub struct UnusedVariable { - pub name: String, - } -); -impl AlwaysAutofixableViolation for UnusedVariable { - #[derive_message_formats] - fn message(&self) -> String { - let UnusedVariable { name } = self; - format!("Local variable `{name}` is assigned to but never used") - } - - fn autofix_title(&self) -> String { - let UnusedVariable { name } = self; - format!("Remove assignment to unused variable `{name}`") - } -} - -define_violation!( - pub struct UnusedAnnotation { - pub name: String, - } -); -impl Violation for UnusedAnnotation { - #[derive_message_formats] - fn message(&self) -> String { - let UnusedAnnotation { name } = self; - format!("Local variable `{name}` is annotated but never used") - } -} - -define_violation!( - pub struct RaiseNotImplemented; -); -impl AlwaysAutofixableViolation for RaiseNotImplemented { - #[derive_message_formats] - fn message(&self) -> String { - format!("`raise NotImplemented` should be `raise NotImplementedError`") - } - - fn autofix_title(&self) -> String { - "Use `raise NotImplementedError`".to_string() - } -} - // pylint define_violation!(