Implement unused argument detection (`ARG`) (#1126)

Detect unused arguments
This commit is contained in:
Charlie Marsh 2022-12-07 12:15:41 -05:00 committed by GitHub
parent d698c6123e
commit bb67fbb73a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 984 additions and 188 deletions

50
LICENSE
View File

@ -388,6 +388,56 @@ are:
SOFTWARE. SOFTWARE.
""" """
- flake8-import-conventions, licensed as follows:
"""
MIT License
Copyright (c) 2021 João Palmeiro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-unused-arguments, licensed as follows:
"""
MIT License
Copyright (c) 2019 Nathan Hoad
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- isort, licensed as follows: - isort, licensed as follows:
""" """
The MIT License (MIT) The MIT License (MIT)

View File

@ -90,6 +90,7 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [flake8-quotes (Q)](#flake8-quotes-q) 1. [flake8-quotes (Q)](#flake8-quotes-q)
1. [flake8-return (RET)](#flake8-return-ret) 1. [flake8-return (RET)](#flake8-return-ret)
1. [flake8-tidy-imports (I25)](#flake8-tidy-imports-i25) 1. [flake8-tidy-imports (I25)](#flake8-tidy-imports-i25)
1. [flake8-unused-arguments (ARG)](#flake8-unused-arguments-arg)
1. [eradicate (ERA)](#eradicate-era) 1. [eradicate (ERA)](#eradicate-era)
1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh) 1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh)
1. [Pylint (PLC, PLE, PLR, PLW)](#pylint-plc-ple-plr-plw) 1. [Pylint (PLC, PLE, PLR, PLW)](#pylint-plc-ple-plr-plw)
@ -768,6 +769,18 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
| ---- | ---- | ------- | --- | | ---- | ---- | ------- | --- |
| I252 | BannedRelativeImport | Relative imports are banned | | | I252 | BannedRelativeImport | Relative imports are banned | |
### flake8-unused-arguments (ARG)
For more, see [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/0.0.12/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| ARG001 | UnusedFunctionArgument | Unused function argument: `...` | |
| ARG002 | UnusedMethodArgument | Unused method argument: `...` | |
| ARG003 | UnusedClassMethodArgument | Unused class method argument: `...` | |
| ARG004 | UnusedStaticMethodArgument | Unused static method argument: `...` | |
| ARG005 | UnusedLambdaArgument | Unused lambda argument: `...` | |
### eradicate (ERA) ### eradicate (ERA)
For more, see [eradicate](https://pypi.org/project/eradicate/2.1.0/) on PyPI. For more, see [eradicate](https://pypi.org/project/eradicate/2.1.0/) on PyPI.

View File

@ -0,0 +1,137 @@
from abc import abstractmethod
from typing_extensions import override
###
# Unused arguments on functions.
###
def f(self, x):
print("Hello, world!")
def f(cls, x):
print("Hello, world!")
def f(self, x):
...
def f(cls, x):
...
###
# Unused arguments on lambdas.
###
lambda x: print("Hello, world!")
class X:
###
# Unused arguments.
###
def f(self, x):
print("Hello, world!")
def f(self, /, x):
print("Hello, world!")
def f(cls, x):
print("Hello, world!")
@classmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
def f(x):
print("Hello, world!")
###
# Unused arguments attached to empty functions (OK).
###
def f(self, x):
...
def f(self, /, x):
...
def f(cls, x):
...
@classmethod
def f(cls, x):
...
@staticmethod
def f(cls, x):
...
@staticmethod
def f(x):
...
###
# Unused functions attached to abstract methods (OK).
###
@abstractmethod
def f(self, x):
print("Hello, world!")
@abstractmethod
def f(self, /, x):
print("Hello, world!")
@abstractmethod
def f(cls, x):
print("Hello, world!")
@classmethod
@abstractmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
@abstractmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
@abstractmethod
def f(x):
print("Hello, world!")
###
# Unused functions attached to overrides (OK).
###
@override
def f(self, x):
print("Hello, world!")
@override
def f(self, /, x):
print("Hello, world!")
@override
def f(cls, x):
print("Hello, world!")
@classmethod
@override
def f(cls, x):
print("Hello, world!")
@staticmethod
@override
def f(cls, x):
print("Hello, world!")
@staticmethod
@override
def f(x):
print("Hello, world!")

9
src/ast/cast.rs Normal file
View File

@ -0,0 +1,9 @@
use rustpython_ast::{Expr, Stmt, StmtKind};
pub fn decorator_list(stmt: &Stmt) -> &Vec<Expr> {
match &stmt.node {
StmtKind::FunctionDef { decorator_list, .. }
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list,
_ => panic!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
}
}

65
src/ast/function_type.rs Normal file
View File

@ -0,0 +1,65 @@
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Expr;
use crate::ast::helpers::{
collect_call_paths, dealias_call_path, match_call_path, to_module_and_member,
};
use crate::ast::types::{Scope, ScopeKind};
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")];
pub enum FunctionType {
Function,
Method,
ClassMethod,
StaticMethod,
}
/// Classify a function based on its scope, name, and decorators.
pub fn classify(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
classmethod_decorators: &[String],
staticmethod_decorators: &[String],
) -> FunctionType {
let ScopeKind::Class(scope) = &scope.kind else {
return FunctionType::Function;
};
// Special-case class method, like `__new__`.
if CLASS_METHODS.contains(&name)
|| scope.bases.iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
METACLASS_BASES
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
})
|| decorator_list.iter().any(|expr| {
// The method is decorated with a class method decorator (like `@classmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
classmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
})
{
FunctionType::ClassMethod
} else if decorator_list.iter().any(|expr| {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
staticmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
}) {
FunctionType::StaticMethod
} else {
// It's an instance method.
FunctionType::Method
}
}

View File

@ -1,3 +1,5 @@
pub mod cast;
pub mod function_type;
pub mod helpers; pub mod helpers;
pub mod operations; pub mod operations;
pub mod relocate; pub mod relocate;

View File

@ -1,7 +1,7 @@
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use rustpython_ast::{Expr, Keyword, Stmt}; use rustpython_ast::{Arguments, Expr, Keyword, Stmt};
use rustpython_parser::ast::{Located, Location}; use rustpython_parser::ast::{Located, Location};
fn id() -> usize { fn id() -> usize {
@ -30,35 +30,49 @@ impl Range {
} }
} }
#[derive(Clone, Debug)] #[derive(Debug)]
pub struct FunctionScope { pub struct FunctionDef<'a> {
pub name: &'a str,
pub args: &'a Arguments,
pub body: &'a [Stmt],
pub decorator_list: &'a [Expr],
// pub returns: Option<&'a Expr>,
// pub type_comment: Option<&'a str>,
// TODO(charlie): Create AsyncFunctionDef to mirror the AST.
pub async_: bool, pub async_: bool,
pub uses_locals: bool,
} }
#[derive(Clone, Debug)] #[derive(Debug)]
pub struct ClassScope<'a> { pub struct ClassDef<'a> {
pub name: &'a str, pub name: &'a str,
pub bases: &'a [Expr], pub bases: &'a [Expr],
pub keywords: &'a [Keyword], pub keywords: &'a [Keyword],
// pub body: &'a [Stmt],
pub decorator_list: &'a [Expr], pub decorator_list: &'a [Expr],
} }
#[derive(Clone, Debug)] #[derive(Debug)]
pub struct Lambda<'a> {
pub args: &'a Arguments,
pub body: &'a Expr,
}
#[derive(Debug)]
pub enum ScopeKind<'a> { pub enum ScopeKind<'a> {
Class(ClassScope<'a>), Class(ClassDef<'a>),
Function(FunctionScope), Function(FunctionDef<'a>),
Generator, Generator,
Module, Module,
Arg, Arg,
Lambda, Lambda(Lambda<'a>),
} }
#[derive(Clone, Debug)] #[derive(Debug)]
pub struct Scope<'a> { pub struct Scope<'a> {
pub id: usize, pub id: usize,
pub kind: ScopeKind<'a>, pub kind: ScopeKind<'a>,
pub import_starred: bool, pub import_starred: bool,
pub uses_locals: bool,
pub values: FxHashMap<&'a str, Binding>, pub values: FxHashMap<&'a str, Binding>,
} }
@ -68,6 +82,7 @@ impl<'a> Scope<'a> {
id: id(), id: id(),
kind, kind,
import_starred: false, import_starred: false,
uses_locals: false,
values: FxHashMap::default(), values: FxHashMap::default(),
} }
} }

View File

@ -17,7 +17,8 @@ use crate::ast::helpers::{
use crate::ast::operations::extract_all_names; use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr; use crate::ast::relocate::relocate_expr;
use crate::ast::types::{ use crate::ast::types::{
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, Node, Range, Scope, ScopeKind, Binding, BindingContext, BindingKind, ClassDef, FunctionDef, Lambda, Node, Range, Scope,
ScopeKind,
}; };
use crate::ast::visitor::{walk_excepthandler, Visitor}; use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{helpers, operations, visitor}; use crate::ast::{helpers, operations, visitor};
@ -35,8 +36,9 @@ use crate::visibility::{module_visibility, transition_scope, Modifier, Visibilit
use crate::{ use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_debugger, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_debugger,
flake8_import_conventions, flake8_print, flake8_return, flake8_tidy_imports, mccabe, flake8_import_conventions, flake8_print, flake8_return, flake8_tidy_imports,
pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, flake8_unused_arguments, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks,
pylint, pyupgrade,
}; };
const GLOBAL_SCOPE_INDEX: usize = 0; const GLOBAL_SCOPE_INDEX: usize = 0;
@ -71,7 +73,7 @@ pub struct Checker<'a> {
deferred_type_definitions: Vec<(&'a Expr, bool, DeferralContext)>, deferred_type_definitions: Vec<(&'a Expr, bool, DeferralContext)>,
deferred_functions: Vec<(&'a Stmt, DeferralContext, VisibleScope)>, deferred_functions: Vec<(&'a Stmt, DeferralContext, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, DeferralContext)>, deferred_lambdas: Vec<(&'a Expr, DeferralContext)>,
deferred_assignments: Vec<usize>, deferred_assignments: Vec<(usize, DeferralContext)>,
// Internal, derivative state. // Internal, derivative state.
visible_scope: VisibleScope, visible_scope: VisibleScope,
in_f_string: Option<Range>, in_f_string: Option<Range>,
@ -580,7 +582,7 @@ where
for expr in decorator_list { for expr in decorator_list {
self.visit_expr(expr); self.visit_expr(expr);
} }
self.push_scope(Scope::new(ScopeKind::Class(ClassScope { self.push_scope(Scope::new(ScopeKind::Class(ClassDef {
name, name,
bases, bases,
keywords, keywords,
@ -1406,20 +1408,18 @@ where
} }
Ok(summary) => { Ok(summary) => {
if self.settings.enabled.contains(&CheckCode::F522) { if self.settings.enabled.contains(&CheckCode::F522) {
if let Some(check) = if let Some(check) = pyflakes::checks::string_dot_format_extra_named_arguments(
pyflakes::checks::string_dot_format_extra_named_arguments( &summary, keywords, location,
&summary, keywords, location, )
)
{ {
self.add_check(check); self.add_check(check);
} }
} }
if self.settings.enabled.contains(&CheckCode::F523) { if self.settings.enabled.contains(&CheckCode::F523) {
if let Some(check) = if let Some(check) = pyflakes::checks::string_dot_format_extra_positional_arguments(
pyflakes::checks::string_dot_format_extra_positional_arguments( &summary, args, location,
&summary, args, location, )
)
{ {
self.add_check(check); self.add_check(check);
} }
@ -1486,7 +1486,7 @@ where
.scope_stack .scope_stack
.iter() .iter()
.rev() .rev()
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda)) .any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda(..)))
{ {
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args); flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
} }
@ -1747,9 +1747,7 @@ where
if id == "locals" && matches!(ctx, ExprContext::Load) { if id == "locals" && matches!(ctx, ExprContext::Load) {
let scope = &mut self.scopes let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found"))]; [*(self.scope_stack.last().expect("No current scope found"))];
if let ScopeKind::Function(inner) = &mut scope.kind { scope.uses_locals = true;
inner.uses_locals = true;
}
} }
} }
@ -2070,7 +2068,7 @@ where
} }
} }
} }
ExprKind::Lambda { args, .. } => { ExprKind::Lambda { args, body, .. } => {
// Visit the arguments, but avoid the body, which will be deferred. // Visit the arguments, but avoid the body, which will be deferred.
for arg in &args.posonlyargs { for arg in &args.posonlyargs {
if let Some(expr) = &arg.node.annotation { if let Some(expr) = &arg.node.annotation {
@ -2103,7 +2101,7 @@ where
for expr in &args.defaults { for expr in &args.defaults {
self.visit_expr(expr); self.visit_expr(expr);
} }
self.push_scope(Scope::new(ScopeKind::Lambda)); self.push_scope(Scope::new(ScopeKind::Lambda(Lambda { args, body })));
} }
ExprKind::ListComp { elt, generators } | ExprKind::SetComp { elt, generators } => { ExprKind::ListComp { elt, generators } | ExprKind::SetComp { elt, generators } => {
if self.settings.enabled.contains(&CheckCode::C416) { if self.settings.enabled.contains(&CheckCode::C416) {
@ -2958,27 +2956,44 @@ impl<'a> Checker<'a> {
fn check_deferred_functions(&mut self) { fn check_deferred_functions(&mut self) {
while let Some((stmt, (scopes, parents), visibility)) = self.deferred_functions.pop() { while let Some((stmt, (scopes, parents), visibility)) = self.deferred_functions.pop() {
self.scope_stack = scopes; self.scope_stack = scopes.clone();
self.parent_stack = parents; self.parent_stack = parents.clone();
self.visible_scope = visibility; self.visible_scope = visibility;
self.push_scope(Scope::new(ScopeKind::Function(FunctionScope {
async_: matches!(stmt.node, StmtKind::AsyncFunctionDef { .. }),
uses_locals: false,
})));
match &stmt.node { match &stmt.node {
StmtKind::FunctionDef { body, args, .. } StmtKind::FunctionDef {
| StmtKind::AsyncFunctionDef { body, args, .. } => { name,
body,
args,
decorator_list,
..
}
| StmtKind::AsyncFunctionDef {
name,
body,
args,
decorator_list,
..
} => {
self.push_scope(Scope::new(ScopeKind::Function(FunctionDef {
name,
body,
args,
decorator_list,
async_: matches!(stmt.node, StmtKind::AsyncFunctionDef { .. }),
})));
self.visit_arguments(args); self.visit_arguments(args);
for stmt in body { for stmt in body {
self.visit_stmt(stmt); self.visit_stmt(stmt);
} }
} }
_ => {} _ => unreachable!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
} }
self.deferred_assignments self.deferred_assignments.push((
.push(*self.scope_stack.last().expect("No current scope found")); *self.scope_stack.last().expect("No current scope found"),
(scopes, parents),
));
self.pop_scope(); self.pop_scope();
} }
@ -2986,25 +3001,29 @@ impl<'a> Checker<'a> {
fn check_deferred_lambdas(&mut self) { fn check_deferred_lambdas(&mut self) {
while let Some((expr, (scopes, parents))) = self.deferred_lambdas.pop() { while let Some((expr, (scopes, parents))) = self.deferred_lambdas.pop() {
self.scope_stack = scopes; self.scope_stack = scopes.clone();
self.parent_stack = parents; self.parent_stack = parents.clone();
self.push_scope(Scope::new(ScopeKind::Lambda));
if let ExprKind::Lambda { args, body } = &expr.node { if let ExprKind::Lambda { args, body } = &expr.node {
self.push_scope(Scope::new(ScopeKind::Lambda(Lambda { args, body })));
self.visit_arguments(args); self.visit_arguments(args);
self.visit_expr(body); self.visit_expr(body);
} else {
unreachable!("Expected ExprKind::Lambda");
} }
self.deferred_assignments self.deferred_assignments.push((
.push(*self.scope_stack.last().expect("No current scope found")); *self.scope_stack.last().expect("No current scope found"),
(scopes, parents),
));
self.pop_scope(); self.pop_scope();
} }
} }
fn check_deferred_assignments(&mut self) { fn check_deferred_assignments(&mut self) {
if self.settings.enabled.contains(&CheckCode::F841) { while let Some((index, (scopes, _parents))) = self.deferred_assignments.pop() {
while let Some(index) = self.deferred_assignments.pop() { if self.settings.enabled.contains(&CheckCode::F841) {
self.add_checks( self.add_checks(
pyflakes::checks::unused_variables( pyflakes::checks::unused_variables(
&self.scopes[index], &self.scopes[index],
@ -3013,6 +3032,23 @@ impl<'a> Checker<'a> {
.into_iter(), .into_iter(),
); );
} }
if self.settings.enabled.contains(&CheckCode::ARG001)
|| self.settings.enabled.contains(&CheckCode::ARG002)
|| self.settings.enabled.contains(&CheckCode::ARG003)
|| self.settings.enabled.contains(&CheckCode::ARG004)
|| self.settings.enabled.contains(&CheckCode::ARG005)
{
self.add_checks(
flake8_unused_arguments::plugins::unused_arguments(
self,
&self.scopes[*scopes
.last()
.expect("Expected parent scope above function scope")],
&self.scopes[index],
)
.into_iter(),
);
}
} }
} }
@ -3092,7 +3128,7 @@ impl<'a> Checker<'a> {
for (name, binding) in &scope.values { for (name, binding) in &scope.values {
let (BindingKind::Importation(_, full_name, context) let (BindingKind::Importation(_, full_name, context)
| BindingKind::SubmoduleImportation(_, full_name, context) | BindingKind::SubmoduleImportation(_, full_name, context)
| BindingKind::FromImportation(_, full_name, context)) = &binding.kind else { continue }; | BindingKind::FromImportation(_, full_name, context)) = &binding.kind else { continue; };
// Skip used exports from `__all__` // Skip used exports from `__all__`
if binding.used.is_some() if binding.used.is_some()

View File

@ -292,6 +292,12 @@ pub enum CheckCode {
FBT001, FBT001,
FBT002, FBT002,
FBT003, FBT003,
// flake8-unused-arguments
ARG001,
ARG002,
ARG003,
ARG004,
ARG005,
// flake8-import-conventions // flake8-import-conventions
ICN001, ICN001,
// Ruff // Ruff
@ -326,6 +332,7 @@ pub enum CheckCategory {
Flake8Quotes, Flake8Quotes,
Flake8Return, Flake8Return,
Flake8TidyImports, Flake8TidyImports,
Flake8UnusedArguments,
Eradicate, Eradicate,
PygrepHooks, PygrepHooks,
Pylint, Pylint,
@ -364,6 +371,7 @@ impl CheckCategory {
CheckCategory::Flake8Quotes => "flake8-quotes", CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Flake8Return => "flake8-return", CheckCategory::Flake8Return => "flake8-return",
CheckCategory::Flake8TidyImports => "flake8-tidy-imports", CheckCategory::Flake8TidyImports => "flake8-tidy-imports",
CheckCategory::Flake8UnusedArguments => "flake8-unused-arguments",
CheckCategory::Isort => "isort", CheckCategory::Isort => "isort",
CheckCategory::McCabe => "mccabe", CheckCategory::McCabe => "mccabe",
CheckCategory::PEP8Naming => "pep8-naming", CheckCategory::PEP8Naming => "pep8-naming",
@ -393,6 +401,7 @@ impl CheckCategory {
CheckCategory::Flake8Quotes => vec![CheckCodePrefix::Q], CheckCategory::Flake8Quotes => vec![CheckCodePrefix::Q],
CheckCategory::Flake8Return => vec![CheckCodePrefix::RET], CheckCategory::Flake8Return => vec![CheckCodePrefix::RET],
CheckCategory::Flake8TidyImports => vec![CheckCodePrefix::I25], CheckCategory::Flake8TidyImports => vec![CheckCodePrefix::I25],
CheckCategory::Flake8UnusedArguments => vec![CheckCodePrefix::ARG],
CheckCategory::Isort => vec![CheckCodePrefix::I00], CheckCategory::Isort => vec![CheckCodePrefix::I00],
CheckCategory::McCabe => vec![CheckCodePrefix::C90], CheckCategory::McCabe => vec![CheckCodePrefix::C90],
CheckCategory::PEP8Naming => vec![CheckCodePrefix::N], CheckCategory::PEP8Naming => vec![CheckCodePrefix::N],
@ -470,6 +479,10 @@ impl CheckCategory {
"https://pypi.org/project/flake8-tidy-imports/4.8.0/", "https://pypi.org/project/flake8-tidy-imports/4.8.0/",
&Platform::PyPI, &Platform::PyPI,
)), )),
CheckCategory::Flake8UnusedArguments => Some((
"https://pypi.org/project/flake8-unused-arguments/0.0.12/",
&Platform::PyPI,
)),
CheckCategory::Isort => { CheckCategory::Isort => {
Some(("https://pypi.org/project/isort/5.10.1/", &Platform::PyPI)) Some(("https://pypi.org/project/isort/5.10.1/", &Platform::PyPI))
} }
@ -817,6 +830,12 @@ pub enum CheckKind {
BooleanPositionalValueInFunctionCall, BooleanPositionalValueInFunctionCall,
// pygrep-hooks // pygrep-hooks
NoEval, NoEval,
// flake8-unused-arguments
UnusedFunctionArgument(String),
UnusedMethodArgument(String),
UnusedClassMethodArgument(String),
UnusedStaticMethodArgument(String),
UnusedLambdaArgument(String),
// flake8-import-conventions // flake8-import-conventions
ImportAliasIsNotConventional(String, String), ImportAliasIsNotConventional(String, String),
// Ruff // Ruff
@ -1159,6 +1178,12 @@ impl CheckCode {
CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall, CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall,
// pygrep-hooks // pygrep-hooks
CheckCode::PGH001 => CheckKind::NoEval, CheckCode::PGH001 => CheckKind::NoEval,
// flake8-unused-arguments
CheckCode::ARG001 => CheckKind::UnusedFunctionArgument("...".to_string()),
CheckCode::ARG002 => CheckKind::UnusedMethodArgument("...".to_string()),
CheckCode::ARG003 => CheckKind::UnusedClassMethodArgument("...".to_string()),
CheckCode::ARG004 => CheckKind::UnusedStaticMethodArgument("...".to_string()),
CheckCode::ARG005 => CheckKind::UnusedLambdaArgument("...".to_string()),
// flake8-import-conventions // flake8-import-conventions
CheckCode::ICN001 => { CheckCode::ICN001 => {
CheckKind::ImportAliasIsNotConventional("...".to_string(), "...".to_string()) CheckKind::ImportAliasIsNotConventional("...".to_string(), "...".to_string())
@ -1188,6 +1213,11 @@ impl CheckCode {
CheckCode::ANN205 => CheckCategory::Flake8Annotations, CheckCode::ANN205 => CheckCategory::Flake8Annotations,
CheckCode::ANN206 => CheckCategory::Flake8Annotations, CheckCode::ANN206 => CheckCategory::Flake8Annotations,
CheckCode::ANN401 => CheckCategory::Flake8Annotations, CheckCode::ANN401 => CheckCategory::Flake8Annotations,
CheckCode::ARG001 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG002 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG003 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG004 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG005 => CheckCategory::Flake8UnusedArguments,
CheckCode::B002 => CheckCategory::Flake8Bugbear, CheckCode::B002 => CheckCategory::Flake8Bugbear,
CheckCode::B003 => CheckCategory::Flake8Bugbear, CheckCode::B003 => CheckCategory::Flake8Bugbear,
CheckCode::B004 => CheckCategory::Flake8Bugbear, CheckCode::B004 => CheckCategory::Flake8Bugbear,
@ -1339,8 +1369,8 @@ impl CheckCode {
CheckCode::FBT002 => CheckCategory::Flake8BooleanTrap, CheckCode::FBT002 => CheckCategory::Flake8BooleanTrap,
CheckCode::FBT003 => CheckCategory::Flake8BooleanTrap, CheckCode::FBT003 => CheckCategory::Flake8BooleanTrap,
CheckCode::I001 => CheckCategory::Isort, CheckCode::I001 => CheckCategory::Isort,
CheckCode::ICN001 => CheckCategory::Flake8ImportConventions,
CheckCode::I252 => CheckCategory::Flake8TidyImports, CheckCode::I252 => CheckCategory::Flake8TidyImports,
CheckCode::ICN001 => CheckCategory::Flake8ImportConventions,
CheckCode::N801 => CheckCategory::PEP8Naming, CheckCode::N801 => CheckCategory::PEP8Naming,
CheckCode::N802 => CheckCategory::PEP8Naming, CheckCode::N802 => CheckCategory::PEP8Naming,
CheckCode::N803 => CheckCategory::PEP8Naming, CheckCode::N803 => CheckCategory::PEP8Naming,
@ -1686,6 +1716,12 @@ impl CheckKind {
CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003, CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003,
// pygrep-hooks // pygrep-hooks
CheckKind::NoEval => &CheckCode::PGH001, CheckKind::NoEval => &CheckCode::PGH001,
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(..) => &CheckCode::ARG001,
CheckKind::UnusedMethodArgument(..) => &CheckCode::ARG002,
CheckKind::UnusedClassMethodArgument(..) => &CheckCode::ARG003,
CheckKind::UnusedStaticMethodArgument(..) => &CheckCode::ARG004,
CheckKind::UnusedLambdaArgument(..) => &CheckCode::ARG005,
// flake8-import-conventions // flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(..) => &CheckCode::ICN001, CheckKind::ImportAliasIsNotConventional(..) => &CheckCode::ICN001,
// Ruff // Ruff
@ -2477,6 +2513,18 @@ impl CheckKind {
} }
// pygrep-hooks // pygrep-hooks
CheckKind::NoEval => "No builtin `eval()` allowed".to_string(), CheckKind::NoEval => "No builtin `eval()` allowed".to_string(),
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(name) => {
format!("Unused function argument: `{name}`")
}
CheckKind::UnusedMethodArgument(name) => format!("Unused method argument: `{name}`"),
CheckKind::UnusedClassMethodArgument(name) => {
format!("Unused class method argument: `{name}`")
}
CheckKind::UnusedStaticMethodArgument(name) => {
format!("Unused static method argument: `{name}`")
}
CheckKind::UnusedLambdaArgument(name) => format!("Unused lambda argument: `{name}`"),
// flake8-import-conventions // flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(name, asname) => { CheckKind::ImportAliasIsNotConventional(name, asname) => {
format!("`{name}` should be imported as `{asname}`") format!("`{name}` should be imported as `{asname}`")

View File

@ -36,6 +36,14 @@ pub enum CheckCodePrefix {
ANN4, ANN4,
ANN40, ANN40,
ANN401, ANN401,
ARG,
ARG0,
ARG00,
ARG001,
ARG002,
ARG003,
ARG004,
ARG005,
B, B,
B0, B0,
B00, B00,
@ -492,6 +500,32 @@ impl CheckCodePrefix {
CheckCodePrefix::ANN4 => vec![CheckCode::ANN401], CheckCodePrefix::ANN4 => vec![CheckCode::ANN401],
CheckCodePrefix::ANN40 => vec![CheckCode::ANN401], CheckCodePrefix::ANN40 => vec![CheckCode::ANN401],
CheckCodePrefix::ANN401 => vec![CheckCode::ANN401], CheckCodePrefix::ANN401 => vec![CheckCode::ANN401],
CheckCodePrefix::ARG => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG0 => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG00 => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG001 => vec![CheckCode::ARG001],
CheckCodePrefix::ARG002 => vec![CheckCode::ARG002],
CheckCodePrefix::ARG003 => vec![CheckCode::ARG003],
CheckCodePrefix::ARG004 => vec![CheckCode::ARG004],
CheckCodePrefix::ARG005 => vec![CheckCode::ARG005],
CheckCodePrefix::B => vec![ CheckCodePrefix::B => vec![
CheckCode::B002, CheckCode::B002,
CheckCode::B003, CheckCode::B003,
@ -1698,6 +1732,14 @@ impl CheckCodePrefix {
CheckCodePrefix::ANN4 => SuffixLength::One, CheckCodePrefix::ANN4 => SuffixLength::One,
CheckCodePrefix::ANN40 => SuffixLength::Two, CheckCodePrefix::ANN40 => SuffixLength::Two,
CheckCodePrefix::ANN401 => SuffixLength::Three, CheckCodePrefix::ANN401 => SuffixLength::Three,
CheckCodePrefix::ARG => SuffixLength::Zero,
CheckCodePrefix::ARG0 => SuffixLength::One,
CheckCodePrefix::ARG00 => SuffixLength::Two,
CheckCodePrefix::ARG001 => SuffixLength::Three,
CheckCodePrefix::ARG002 => SuffixLength::Three,
CheckCodePrefix::ARG003 => SuffixLength::Three,
CheckCodePrefix::ARG004 => SuffixLength::Three,
CheckCodePrefix::ARG005 => SuffixLength::Three,
CheckCodePrefix::B => SuffixLength::Zero, CheckCodePrefix::B => SuffixLength::Zero,
CheckCodePrefix::B0 => SuffixLength::One, CheckCodePrefix::B0 => SuffixLength::One,
CheckCodePrefix::B00 => SuffixLength::Two, CheckCodePrefix::B00 => SuffixLength::Two,
@ -2096,6 +2138,7 @@ impl CheckCodePrefix {
pub const CATEGORIES: &[CheckCodePrefix] = &[ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::A, CheckCodePrefix::A,
CheckCodePrefix::ANN, CheckCodePrefix::ANN,
CheckCodePrefix::ARG,
CheckCodePrefix::B, CheckCodePrefix::B,
CheckCodePrefix::BLE, CheckCodePrefix::BLE,
CheckCodePrefix::C, CheckCodePrefix::C,

View File

@ -1,5 +1,6 @@
use rustpython_ast::{Arguments, Expr, Stmt, StmtKind}; use rustpython_ast::{Arguments, Expr, Stmt, StmtKind};
use crate::ast::cast;
use crate::check_ast::Checker; use crate::check_ast::Checker;
use crate::docstrings::definition::{Definition, DefinitionKind}; use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::visibility; use crate::visibility;
@ -32,7 +33,7 @@ pub fn overloaded_name(checker: &Checker, definition: &Definition) -> Option<Str
| DefinitionKind::NestedFunction(stmt) | DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind | DefinitionKind::Method(stmt) = definition.kind
{ {
if visibility::is_overload(checker, stmt) { if visibility::is_overload(checker, cast::decorator_list(stmt)) {
let (name, ..) = match_function_def(stmt); let (name, ..) = match_function_def(stmt);
Some(name.to_string()) Some(name.to_string())
} else { } else {
@ -50,7 +51,7 @@ pub fn is_overload_impl(checker: &Checker, definition: &Definition, overloaded_n
| DefinitionKind::NestedFunction(stmt) | DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind | DefinitionKind::Method(stmt) = definition.kind
{ {
if visibility::is_overload(checker, stmt) { if visibility::is_overload(checker, cast::decorator_list(stmt)) {
false false
} else { } else {
let (name, ..) = match_function_def(stmt); let (name, ..) = match_function_def(stmt);

View File

@ -1,8 +1,8 @@
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind}; use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range; use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor; use crate::ast::visitor::Visitor;
use crate::ast::{cast, visitor};
use crate::check_ast::Checker; use crate::check_ast::Checker;
use crate::checks::{CheckCode, CheckKind}; use crate::checks::{CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind}; use crate::docstrings::definition::{Definition, DefinitionKind};
@ -192,7 +192,10 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
.chain(args.kwonlyargs.iter()) .chain(args.kwonlyargs.iter())
.skip( .skip(
// If this is a non-static method, skip `cls` or `self`. // If this is a non-static method, skip `cls` or `self`.
usize::from(!visibility::is_staticmethod(checker, stmt)), usize::from(!visibility::is_staticmethod(
checker,
cast::decorator_list(stmt),
)),
) )
{ {
// ANN401 for dynamically typed arguments // ANN401 for dynamically typed arguments
@ -264,10 +267,10 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
} }
// ANN101, ANN102 // ANN101, ANN102
if !visibility::is_staticmethod(checker, stmt) { if !visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if let Some(arg) = args.args.first() { if let Some(arg) = args.args.first() {
if arg.node.annotation.is_none() { if arg.node.annotation.is_none() {
if visibility::is_classmethod(checker, stmt) { if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN102) { if checker.settings.enabled.contains(&CheckCode::ANN102) {
checker.add_check(Check::new( checker.add_check(Check::new(
CheckKind::MissingTypeCls(arg.node.arg.to_string()), CheckKind::MissingTypeCls(arg.node.arg.to_string()),
@ -300,14 +303,14 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
return; return;
} }
if visibility::is_classmethod(checker, stmt) { if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN206) { if checker.settings.enabled.contains(&CheckCode::ANN206) {
checker.add_check(Check::new( checker.add_check(Check::new(
CheckKind::MissingReturnTypeClassMethod(name.to_string()), CheckKind::MissingReturnTypeClassMethod(name.to_string()),
Range::from_located(stmt), Range::from_located(stmt),
)); ));
} }
} else if visibility::is_staticmethod(checker, stmt) { } else if visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN205) { if checker.settings.enabled.contains(&CheckCode::ANN205) {
checker.add_check(Check::new( checker.add_check(Check::new(
CheckKind::MissingReturnTypeStaticMethod(name.to_string()), CheckKind::MissingReturnTypeStaticMethod(name.to_string()),

View File

@ -0,0 +1,19 @@
use rustpython_ast::{Constant, ExprKind, Stmt, StmtKind};
pub fn is_empty(body: &[Stmt]) -> bool {
match &body {
[] => true,
// Also allow: raise NotImplementedError, raise NotImplemented
[stmt] => match &stmt.node {
StmtKind::Pass => true,
StmtKind::Expr { value } => match &value.node {
ExprKind::Constant { value, .. } => {
matches!(value, Constant::Str(_) | Constant::Ellipsis)
}
_ => false,
},
_ => false,
},
_ => false,
}
}

View File

@ -0,0 +1,35 @@
mod helpers;
pub mod plugins;
mod types;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::settings;
#[test_case(CheckCode::ARG001, Path::new("ARG.py"); "ARG001")]
#[test_case(CheckCode::ARG002, Path::new("ARG.py"); "ARG002")]
#[test_case(CheckCode::ARG003, Path::new("ARG.py"); "ARG003")]
#[test_case(CheckCode::ARG004, Path::new("ARG.py"); "ARG004")]
#[test_case(CheckCode::ARG005, Path::new("ARG.py"); "ARG005")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_unused_arguments")
.join(path)
.as_path(),
&settings::Settings::for_rule(check_code),
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

View File

@ -0,0 +1,184 @@
use std::iter;
use regex::Regex;
use rustc_hash::FxHashMap;
use rustpython_ast::{Arg, Arguments};
use crate::ast::function_type;
use crate::ast::function_type::FunctionType;
use crate::ast::helpers::collect_arg_names;
use crate::ast::types::{Binding, BindingKind, FunctionDef, Lambda, Scope, ScopeKind};
use crate::check_ast::Checker;
use crate::flake8_unused_arguments::helpers;
use crate::flake8_unused_arguments::types::Argumentable;
use crate::{visibility, Check};
/// Check a plain function for unused arguments.
fn function(
argumentable: &Argumentable,
args: &Arguments,
bindings: &FxHashMap<&str, Binding>,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for arg_name in collect_arg_names(args) {
if let Some(binding) = bindings.get(arg_name) {
if binding.used.is_none()
&& matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg_name)
{
checks.push(Check::new(
argumentable.check_for(arg_name.to_string()),
binding.range,
));
}
}
}
checks
}
/// Check a method for unused arguments.
fn method(
argumentable: &Argumentable,
args: &Arguments,
bindings: &FxHashMap<&str, Binding>,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for arg in args
.posonlyargs
.iter()
.chain(args.args.iter())
.skip(1)
.chain(args.kwonlyargs.iter())
.chain(iter::once::<Option<&Arg>>(args.vararg.as_deref()).flatten())
.chain(iter::once::<Option<&Arg>>(args.kwarg.as_deref()).flatten())
{
if let Some(binding) = bindings.get(&arg.node.arg.as_str()) {
if binding.used.is_none()
&& matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg.node.arg.as_str())
{
checks.push(Check::new(
argumentable.check_for(arg.node.arg.to_string()),
binding.range,
));
}
}
}
checks
}
/// ARG001, ARG002, ARG003, ARG004, ARG005
pub fn unused_arguments(checker: &Checker, parent: &Scope, scope: &Scope) -> Vec<Check> {
match &scope.kind {
ScopeKind::Function(FunctionDef {
name,
args,
body,
decorator_list,
..
}) => {
match function_type::classify(
parent,
name,
decorator_list,
&checker.from_imports,
&checker.import_aliases,
&checker.settings.pep8_naming.classmethod_decorators,
&checker.settings.pep8_naming.staticmethod_decorators,
) {
FunctionType::Function => {
if checker
.settings
.enabled
.contains(Argumentable::Function.check_code())
{
function(
&Argumentable::Function,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::Method => {
if checker
.settings
.enabled
.contains(Argumentable::Method.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
method(
&Argumentable::Method,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::ClassMethod => {
if checker
.settings
.enabled
.contains(Argumentable::ClassMethod.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
method(
&Argumentable::ClassMethod,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::StaticMethod => {
if checker
.settings
.enabled
.contains(Argumentable::StaticMethod.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
function(
&Argumentable::StaticMethod,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
}
}
ScopeKind::Lambda(Lambda { args, .. }) => {
if checker
.settings
.enabled
.contains(Argumentable::Lambda.check_code())
{
function(
&Argumentable::Lambda,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
_ => unreachable!("Expected ScopeKind::Function | ScopeKind::Lambda"),
}
}

View File

@ -0,0 +1,77 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedFunctionArgument: self
location:
row: 8
column: 6
end_location:
row: 8
column: 10
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 8
column: 12
end_location:
row: 8
column: 13
fix: ~
- kind:
UnusedFunctionArgument: cls
location:
row: 12
column: 6
end_location:
row: 12
column: 9
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 12
column: 11
end_location:
row: 12
column: 12
fix: ~
- kind:
UnusedFunctionArgument: self
location:
row: 16
column: 6
end_location:
row: 16
column: 10
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 16
column: 12
end_location:
row: 16
column: 13
fix: ~
- kind:
UnusedFunctionArgument: cls
location:
row: 20
column: 6
end_location:
row: 20
column: 9
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 20
column: 11
end_location:
row: 20
column: 12
fix: ~

View File

@ -0,0 +1,32 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedMethodArgument: x
location:
row: 34
column: 16
end_location:
row: 34
column: 17
fix: ~
- kind:
UnusedMethodArgument: x
location:
row: 37
column: 19
end_location:
row: 37
column: 20
fix: ~
- kind:
UnusedMethodArgument: x
location:
row: 40
column: 15
end_location:
row: 40
column: 16
fix: ~

View File

@ -0,0 +1,14 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedClassMethodArgument: x
location:
row: 44
column: 15
end_location:
row: 44
column: 16
fix: ~

View File

@ -0,0 +1,32 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedStaticMethodArgument: cls
location:
row: 48
column: 10
end_location:
row: 48
column: 13
fix: ~
- kind:
UnusedStaticMethodArgument: x
location:
row: 48
column: 15
end_location:
row: 48
column: 16
fix: ~
- kind:
UnusedStaticMethodArgument: x
location:
row: 52
column: 10
end_location:
row: 52
column: 11
fix: ~

View File

@ -0,0 +1,14 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedLambdaArgument: x
location:
row: 27
column: 7
end_location:
row: 27
column: 8
fix: ~

View File

@ -0,0 +1,32 @@
use crate::checks::{CheckCode, CheckKind};
/// An AST node that can contain arguments.
pub enum Argumentable {
Function,
Method,
ClassMethod,
StaticMethod,
Lambda,
}
impl Argumentable {
pub fn check_for(&self, name: String) -> CheckKind {
match self {
Argumentable::Function => CheckKind::UnusedFunctionArgument(name),
Argumentable::Method => CheckKind::UnusedMethodArgument(name),
Argumentable::ClassMethod => CheckKind::UnusedClassMethodArgument(name),
Argumentable::StaticMethod => CheckKind::UnusedStaticMethodArgument(name),
Argumentable::Lambda => CheckKind::UnusedLambdaArgument(name),
}
}
pub fn check_code(&self) -> &CheckCode {
match self {
Argumentable::Function => &CheckCode::ARG001,
Argumentable::Method => &CheckCode::ARG002,
Argumentable::ClassMethod => &CheckCode::ARG003,
Argumentable::StaticMethod => &CheckCode::ARG004,
Argumentable::Lambda => &CheckCode::ARG005,
}
}
}

View File

@ -54,6 +54,7 @@ mod flake8_print;
pub mod flake8_quotes; pub mod flake8_quotes;
mod flake8_return; mod flake8_return;
pub mod flake8_tidy_imports; pub mod flake8_tidy_imports;
mod flake8_unused_arguments;
pub mod fs; pub mod fs;
mod isort; mod isort;
mod lex; mod lex;

View File

@ -1,10 +1,10 @@
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt}; use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::function_type;
use crate::ast::types::{Range, Scope, ScopeKind}; use crate::ast::types::{Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind}; use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers; use crate::pep8_naming::helpers;
use crate::pep8_naming::helpers::FunctionType;
use crate::pep8_naming::settings::Settings; use crate::pep8_naming::settings::Settings;
use crate::python::string::{self}; use crate::python::string::{self};
@ -58,15 +58,16 @@ pub fn invalid_first_argument_name_for_class_method(
settings: &Settings, settings: &Settings,
) -> Option<Check> { ) -> Option<Check> {
if !matches!( if !matches!(
helpers::function_type( function_type::classify(
scope, scope,
name, name,
decorator_list, decorator_list,
from_imports, from_imports,
import_aliases, import_aliases,
settings, &settings.classmethod_decorators,
&settings.staticmethod_decorators,
), ),
FunctionType::ClassMethod function_type::FunctionType::ClassMethod
) { ) {
return None; return None;
} }
@ -99,15 +100,16 @@ pub fn invalid_first_argument_name_for_method(
settings: &Settings, settings: &Settings,
) -> Option<Check> { ) -> Option<Check> {
if !matches!( if !matches!(
helpers::function_type( function_type::classify(
scope, scope,
name, name,
decorator_list, decorator_list,
from_imports, from_imports,
import_aliases, import_aliases,
settings, &settings.classmethod_decorators,
&settings.staticmethod_decorators,
), ),
FunctionType::Method function_type::FunctionType::Method
) { ) {
return None; return None;
} }

View File

@ -1,71 +1,10 @@
use itertools::Itertools; use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Expr, Stmt, StmtKind}; use rustpython_ast::{Stmt, StmtKind};
use crate::ast::helpers::{ use crate::ast::helpers::{collect_call_paths, match_call_path};
collect_call_paths, dealias_call_path, match_call_path, to_module_and_member,
};
use crate::ast::types::{Scope, ScopeKind};
use crate::pep8_naming::settings::Settings;
use crate::python::string::{is_lower, is_upper}; use crate::python::string::{is_lower, is_upper};
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")];
pub enum FunctionType {
Function,
Method,
ClassMethod,
StaticMethod,
}
/// Classify a function based on its scope, name, and decorators.
pub fn function_type(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
settings: &Settings,
) -> FunctionType {
let ScopeKind::Class(scope) = &scope.kind else {
return FunctionType::Function;
};
// Special-case class method, like `__new__`.
if CLASS_METHODS.contains(&name)
|| scope.bases.iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
METACLASS_BASES
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
})
|| decorator_list.iter().any(|expr| {
// The method is decorated with a class method decorator (like `@classmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
settings.classmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
})
{
FunctionType::ClassMethod
} else if decorator_list.iter().any(|expr| {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
settings.staticmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
}) {
FunctionType::StaticMethod
} else {
// It's an instance method.
FunctionType::Method
}
}
pub fn is_camelcase(name: &str) -> bool { pub fn is_camelcase(name: &str) -> bool {
!is_lower(name) && !is_upper(name) && !name.contains('_') !is_lower(name) && !is_upper(name) && !name.contains('_')
} }

View File

@ -7,8 +7,8 @@ use rustc_hash::FxHashSet;
use rustpython_ast::{Constant, ExprKind, Location, StmtKind}; use rustpython_ast::{Constant, ExprKind, Location, StmtKind};
use crate::ast::types::Range; use crate::ast::types::Range;
use crate::ast::whitespace;
use crate::ast::whitespace::LinesWithTrailingNewline; use crate::ast::whitespace::LinesWithTrailingNewline;
use crate::ast::{cast, whitespace};
use crate::autofix::Fix; use crate::autofix::Fix;
use crate::check_ast::Checker; use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind}; use crate::checks::{Check, CheckCode, CheckKind};
@ -77,7 +77,7 @@ pub fn not_missing(
false false
} }
DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => { DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => {
if is_overload(checker, stmt) { if is_overload(checker, cast::decorator_list(stmt)) {
true true
} else { } else {
if checker.settings.enabled.contains(&CheckCode::D103) { if checker.settings.enabled.contains(&CheckCode::D103) {
@ -90,7 +90,9 @@ pub fn not_missing(
} }
} }
DefinitionKind::Method(stmt) => { DefinitionKind::Method(stmt) => {
if is_overload(checker, stmt) || is_override(checker, stmt) { if is_overload(checker, cast::decorator_list(stmt))
|| is_override(checker, cast::decorator_list(stmt))
{
true true
} else if is_magic(stmt) { } else if is_magic(stmt) {
if checker.settings.enabled.contains(&CheckCode::D105) { if checker.settings.enabled.contains(&CheckCode::D105) {
@ -916,7 +918,7 @@ pub fn if_needed(checker: &mut Checker, definition: &Definition) {
) = definition.kind else { ) = definition.kind else {
return return
}; };
if !is_overload(checker, stmt) { if !is_overload(checker, cast::decorator_list(stmt)) {
return; return;
} }
checker.add_check(Check::new( checker.add_check(Check::new(
@ -1410,7 +1412,7 @@ fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args:
// If this is a non-static method, skip `cls` or `self`. // If this is a non-static method, skip `cls` or `self`.
usize::from( usize::from(
matches!(definition.kind, DefinitionKind::Method(_)) matches!(definition.kind, DefinitionKind::Method(_))
&& !is_staticmethod(checker, parent), && !is_staticmethod(checker, cast::decorator_list(parent)),
), ),
) )
{ {

View File

@ -7,7 +7,7 @@ use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind, Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
}; };
use crate::ast::types::{BindingKind, FunctionScope, Range, Scope, ScopeKind}; use crate::ast::types::{BindingKind, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind}; use crate::checks::{Check, CheckKind};
use crate::pyflakes::cformat::CFormatSummary; use crate::pyflakes::cformat::CFormatSummary;
use crate::pyflakes::format::FormatSummary; use crate::pyflakes::format::FormatSummary;
@ -391,13 +391,7 @@ pub fn undefined_local(scopes: &[&Scope], name: &str) -> Option<Check> {
pub fn unused_variables(scope: &Scope, dummy_variable_rgx: &Regex) -> Vec<Check> { pub fn unused_variables(scope: &Scope, dummy_variable_rgx: &Regex) -> Vec<Check> {
let mut checks: Vec<Check> = vec![]; let mut checks: Vec<Check> = vec![];
if matches!( if scope.uses_locals && matches!(scope.kind, ScopeKind::Function(..)) {
scope.kind,
ScopeKind::Function(FunctionScope {
uses_locals: true,
..
})
) {
return checks; return checks;
} }

View File

@ -1,6 +1,6 @@
use rustpython_ast::Expr; use rustpython_ast::Expr;
use crate::ast::types::{FunctionScope, Range, ScopeKind}; use crate::ast::types::{FunctionDef, Range, ScopeKind};
use crate::check_ast::Checker; use crate::check_ast::Checker;
use crate::checks::CheckKind; use crate::checks::CheckKind;
use crate::Check; use crate::Check;
@ -10,7 +10,7 @@ pub fn await_outside_async(checker: &mut Checker, expr: &Expr) {
if !checker if !checker
.current_scopes() .current_scopes()
.find_map(|scope| { .find_map(|scope| {
if let ScopeKind::Function(FunctionScope { async_, .. }) = &scope.kind { if let ScopeKind::Function(FunctionDef { async_, .. }) = &scope.kind {
Some(*async_) Some(*async_)
} else { } else {
None None

View File

@ -3,7 +3,7 @@
use std::path::Path; use std::path::Path;
use rustpython_ast::{Stmt, StmtKind}; use rustpython_ast::{Expr, Stmt, StmtKind};
use crate::ast::helpers::match_module_member; use crate::ast::helpers::match_module_member;
use crate::check_ast::Checker; use crate::check_ast::Checker;
@ -29,59 +29,56 @@ pub struct VisibleScope {
} }
/// Returns `true` if a function is a "static method". /// Returns `true` if a function is a "static method".
pub fn is_staticmethod(checker: &Checker, stmt: &Stmt) -> bool { pub fn is_staticmethod(checker: &Checker, decorator_list: &[Expr]) -> bool {
match &stmt.node { decorator_list.iter().any(|expr| {
StmtKind::FunctionDef { decorator_list, .. } match_module_member(
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list.iter().any(|expr| { expr,
match_module_member( "",
expr, "staticmethod",
"", &checker.from_imports,
"staticmethod", &checker.import_aliases,
&checker.from_imports, )
&checker.import_aliases, })
)
}),
_ => panic!("Found non-FunctionDef in is_staticmethod"),
}
} }
/// Returns `true` if a function is a "class method". /// Returns `true` if a function is a "class method".
pub fn is_classmethod(checker: &Checker, stmt: &Stmt) -> bool { pub fn is_classmethod(checker: &Checker, decorator_list: &[Expr]) -> bool {
match &stmt.node { decorator_list.iter().any(|expr| {
StmtKind::FunctionDef { decorator_list, .. } match_module_member(
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list.iter().any(|expr| { expr,
match_module_member( "",
expr, "classmethod",
"", &checker.from_imports,
"classmethod", &checker.import_aliases,
&checker.from_imports, )
&checker.import_aliases, })
)
}),
_ => panic!("Found non-FunctionDef in is_classmethod"),
}
} }
/// Returns `true` if a function definition is an `@overload`. /// Returns `true` if a function definition is an `@overload`.
pub fn is_overload(checker: &Checker, stmt: &Stmt) -> bool { pub fn is_overload(checker: &Checker, decorator_list: &[Expr]) -> bool {
match &stmt.node { decorator_list
StmtKind::FunctionDef { decorator_list, .. } .iter()
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list .any(|expr| checker.match_typing_expr(expr, "overload"))
.iter()
.any(|expr| checker.match_typing_expr(expr, "overload")),
_ => panic!("Found non-FunctionDef in is_overload"),
}
} }
/// Returns `true` if a function definition is an `@override` (PEP 698). /// Returns `true` if a function definition is an `@override` (PEP 698).
pub fn is_override(checker: &Checker, stmt: &Stmt) -> bool { pub fn is_override(checker: &Checker, decorator_list: &[Expr]) -> bool {
match &stmt.node { decorator_list
StmtKind::FunctionDef { decorator_list, .. } .iter()
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list .any(|expr| checker.match_typing_expr(expr, "override"))
.iter() }
.any(|expr| checker.match_typing_expr(expr, "override")),
_ => panic!("Found non-FunctionDef in is_override"), /// Returns `true` if a function definition is an `@abstractmethod`.
} pub fn is_abstract(checker: &Checker, decorator_list: &[Expr]) -> bool {
decorator_list.iter().any(|expr| {
match_module_member(
expr,
"abc",
"abstractmethod",
&checker.from_imports,
&checker.import_aliases,
)
})
} }
/// Returns `true` if a function is a "magic method". /// Returns `true` if a function is a "magic method".