Compare commits

..

28 Commits

Author SHA1 Message Date
Charlie Marsh
dd496c7b52 Bump version to 0.0.60 2022-10-07 17:36:33 -04:00
Charlie Marsh
78aafb4b34 Warn the user if an explicitly selected check code is ignored (#356) 2022-10-07 17:36:17 -04:00
Charlie Marsh
99c66d513a Rename refactor checks to upgrade checks (#354) 2022-10-07 16:54:55 -04:00
Charlie Marsh
04ade6a2f3 Update README.md 2022-10-07 16:50:17 -04:00
Charlie Marsh
4cf2682cda Implement type(primitive) (#353) 2022-10-07 16:47:06 -04:00
Charlie Marsh
e3a7357187 Wrap each import in its own backticks (#346) 2022-10-07 15:58:30 -04:00
Charlie Marsh
b60768debb Remove erroneous output.py file 2022-10-07 15:04:23 -04:00
Charlie Marsh
25e476639f Use or_default in lieu of or_insert 2022-10-07 14:56:56 -04:00
Charlie Marsh
4645788205 Bump version to 0.0.59 2022-10-07 14:55:23 -04:00
Charlie Marsh
0b9eda8836 Add target Python version as a configurable setting (#344) 2022-10-07 14:54:50 -04:00
Charlie Marsh
ad23d6acee Remove :: prefix for ruff imports 2022-10-07 14:24:51 -04:00
Harutaka Kawamura
e3d1d01a1f Implement C401 (#343) 2022-10-07 13:00:22 -04:00
Harutaka Kawamura
6dfdd21a7c Implement C400 (#340) 2022-10-07 12:16:23 -04:00
Charlie Marsh
f17d3b3c44 Bump version to 0.0.58 2022-10-07 12:14:03 -04:00
Charlie Marsh
da6b913317 Exit 0 if all errors are fixed (#342) 2022-10-07 12:13:15 -04:00
Harutaka Kawamura
bd34850f98 Implement C404 (#338) 2022-10-07 08:53:18 -04:00
Charlie Marsh
d5b33cdb40 Add missing snapshot for U002 2022-10-07 08:48:59 -04:00
Charlie Marsh
3fb4cf7009 Hide autoformat argument 2022-10-06 22:56:01 -04:00
Charlie Marsh
6e19539e28 Enable abspath(__file__) removal (#336) 2022-10-06 22:49:06 -04:00
Charlie Marsh
4eac7a07f5 Mention flake8-comprehensions in README 2022-10-06 16:26:21 -04:00
Harutaka Kawamura
5141285c8e Implement C403 (#335) 2022-10-06 16:24:23 -04:00
Charlie Marsh
82cc139d2d Bump version to 0.0.57 2022-10-06 09:16:56 -04:00
Adrian Garcia Badaracco
df438ba051 Support PEP 593 annotations (#333) 2022-10-06 09:16:07 -04:00
Adrian Garcia Badaracco
a70624cd47 add instructions for setting up cargo insta (#334) 2022-10-06 08:01:11 -04:00
Charlie Marsh
b307afc00c Move some accesses behind a shared function 2022-10-05 14:54:31 -04:00
Charlie Marsh
3d5bc1f51f Migrate Checker logic to independent plugins (#331) 2022-10-05 14:08:40 -04:00
Charlie Marsh
aba01745f5 Bump version to 0.0.56 2022-10-05 11:58:54 -04:00
Charlie Marsh
1eeeffab66 Add T201 and T203 to string conversion match (#332) 2022-10-05 11:58:33 -04:00
47 changed files with 1409 additions and 446 deletions

View File

@@ -14,6 +14,12 @@ your proposed change.
ruff is written in Rust (1.63.0). You'll need to install the
[Rust toolchain](https://www.rust-lang.org/tools/install) for development.
You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
```shell
cargo install cargo-insta
```
### Development
After cloning the repository, run ruff locally with:
@@ -58,7 +64,7 @@ similar rules are implemented.
To add a test fixture, create a file under `resources/test/fixtures`, named to match the `CheckCode`
you defined earlier (e.g., `E402.py`). This file should contain a variety of violations and
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py`. Once you're satisified with the output,
with (e.g.) `cargo run resources/test/fixtures/E402.py`. Once you're satisfied with the output,
codify the behavior as a snapshot test by adding a new function to the `mod tests` section of
`src/linter.rs`, like so:

2
Cargo.lock generated
View File

@@ -1907,7 +1907,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.55"
version = "0.0.60"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.55"
version = "0.0.60"
edition = "2021"
[lib]

View File

@@ -130,6 +130,8 @@ Options:
Enable automatic additions of noqa directives to failing lines
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
-h, --help
Print help information
-V, --version
@@ -213,6 +215,7 @@ ruff also implements some of the most popular Flake8 plugins natively, including
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (partial)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (partial)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
@@ -274,12 +277,18 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | | |
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | | |
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | | |
| C400 | UnnecessaryGeneratorList | Unnecessary generator - rewrite as a list comprehension | | |
| C401 | UnnecessaryGeneratorSet | Unnecessary generator - rewrite as a set comprehension | | |
| C403 | UnnecessaryListComprehensionSet | Unnecessary list comprehension - rewrite as a set comprehension | | |
| C404 | UnnecessaryListComprehensionDict | Unnecessary list comprehension - rewrite as a dict comprehension | | |
| SPR001 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | | 🛠 |
| T201 | PrintFound | `print` found | | 🛠 |
| T203 | PPrintFound | `pprint` found | | 🛠 |
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | | 🛠 |
| R001 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | | 🛠 |
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | | 🛠 |
| U004 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
| U005 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |
## Integrations

View File

@@ -1,37 +0,0 @@
from collections import Counter
def f() -> None:
"""Docstring goes here."""
for x in range(5):
print(x)
else:
print("Nope!")
a = {
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
}
cls(title=title, before_text=before_text, after_text=after_text, before_description=before_description, after_description=after_description, transform_type=transform_type)

1
resources/test/fixtures/C400.py vendored Normal file
View File

@@ -0,0 +1 @@
x = list(x for x in range(3))

1
resources/test/fixtures/C401.py vendored Normal file
View File

@@ -0,0 +1 @@
x = set(x for x in range(3))

1
resources/test/fixtures/C403.py vendored Normal file
View File

@@ -0,0 +1 @@
s = set([x for x in range(3)])

1
resources/test/fixtures/C404.py vendored Normal file
View File

@@ -0,0 +1 @@
d = dict([(i, i) for i in range(3)])

View File

@@ -89,3 +89,36 @@ A = (
f'B'
f'{B}'
)
from typing import Annotated, Literal
def arbitrary_callable() -> None:
...
class PEP593Test:
field: Annotated[
int,
"base64",
arbitrary_callable(),
123,
(1, 2, 3),
]
field_with_stringified_type: Annotated[
"PEP593Test",
123,
]
field_with_undefined_stringified_type: Annotated[
"PEP593Test123",
123,
]
field_with_nested_subscript: Annotated[
dict[Literal["foo"], str],
123,
]
field_with_undefined_nested_subscript: Annotated[
dict["foo", "bar"], # Expected to fail as undefined.
123,
]

15
resources/test/fixtures/U002.py vendored Normal file
View File

@@ -0,0 +1,15 @@
from os.path import abspath
x = abspath(__file__)
import os
y = os.path.abspath(__file__)
from os import path
z = path.abspath(__file__)

5
resources/test/fixtures/U003.py vendored Normal file
View File

@@ -0,0 +1,5 @@
type('')
type(b'')
type(0)
type(0.)
type(0j)

View File

@@ -4,15 +4,14 @@ use itertools::izip;
use regex::Regex;
use rustpython_parser::ast::{
Arg, ArgData, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind,
Keyword, Location, Stmt, StmtKind, Unaryop,
Stmt, StmtKind, Unaryop,
};
use serde::{Deserialize, Serialize};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{
Binding, BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind,
};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckKind, Fix, RejectedCmpop};
use crate::checks::{Check, CheckKind, RejectedCmpop};
use crate::python::builtins::BUILTINS;
/// Check IfTuple compliance.
@@ -136,6 +135,86 @@ pub fn check_useless_metaclass_type(
None
}
/// Check UnnecessaryAbspath compliance.
pub fn check_unnecessary_abspath(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &args[0].node {
if id == "__file__" {
match &func.node {
ExprKind::Attribute { attr: id, .. } | ExprKind::Name { id, .. } => {
if id == "abspath" {
return Some(Check::new(CheckKind::UnnecessaryAbspath, location));
}
}
_ => {}
}
}
}
}
None
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Primitive {
Bool,
Str,
Bytes,
Int,
Float,
Complex,
}
impl Primitive {
fn from_constant(constant: &Constant) -> Option<Self> {
match constant {
Constant::Bool(_) => Some(Primitive::Bool),
Constant::Str(_) => Some(Primitive::Str),
Constant::Bytes(_) => Some(Primitive::Bytes),
Constant::Int(_) => Some(Primitive::Int),
Constant::Float(_) => Some(Primitive::Float),
Constant::Complex { .. } => Some(Primitive::Complex),
_ => None,
}
}
pub fn builtin(&self) -> String {
match self {
Primitive::Bool => "bool".to_string(),
Primitive::Str => "str".to_string(),
Primitive::Bytes => "bytes".to_string(),
Primitive::Int => "int".to_string(),
Primitive::Float => "float".to_string(),
Primitive::Complex => "complex".to_string(),
}
}
}
/// Check TypeOfPrimitive compliance.
pub fn check_type_of_primitive(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
match &func.node {
ExprKind::Attribute { attr: id, .. } | ExprKind::Name { id, .. } => {
if id == "type" {
if let ExprKind::Constant { value, .. } = &args[0].node {
if let Some(primitive) = Primitive::from_constant(value) {
return Some(Check::new(
CheckKind::TypeOfPrimitive(primitive),
location,
));
}
}
}
}
_ => {}
}
}
None
}
fn is_ambiguous_name(name: &str) -> bool {
name == "l" || name == "I" || name == "O"
}
@@ -178,13 +257,9 @@ pub fn check_ambiguous_function_name(name: &str, location: Range) -> Option<Chec
/// Check UselessObjectInheritance compliance.
pub fn check_useless_object_inheritance(
stmt: &Stmt,
name: &str,
bases: &[Expr],
keywords: &[Keyword],
scope: &Scope,
locator: &mut SourceCodeLocator,
autofix: &fixer::Mode,
) -> Option<Check> {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
@@ -195,22 +270,10 @@ pub fn check_useless_object_inheritance(
kind: BindingKind::Builtin,
..
}) => {
let mut check = Check::new(
return Some(Check::new(
CheckKind::UselessObjectInheritance(name.to_string()),
Range::from_located(expr),
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_class_def_base(
locator,
&stmt.location,
expr.location,
bases,
keywords,
) {
check.amend(fix);
}
}
return Some(check);
));
}
_ => {}
}
@@ -298,28 +361,15 @@ pub fn check_duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
}
/// Check AssertEquals compliance.
pub fn check_assert_equals(expr: &Expr, autofix: &fixer::Mode) -> Option<Check> {
pub fn check_assert_equals(expr: &Expr) -> Option<Check> {
if let ExprKind::Attribute { value, attr, .. } = &expr.node {
if attr == "assertEquals" {
if let ExprKind::Name { id, .. } = &value.node {
if id == "self" {
let mut check =
Check::new(CheckKind::NoAssertEquals, Range::from_located(expr));
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "assertEqual".to_string(),
location: Location::new(
expr.location.row(),
expr.location.column() + 1,
),
end_location: Location::new(
expr.location.row(),
expr.location.column() + 1 + "assertEquals".len(),
),
applied: false,
});
}
return Some(check);
return Some(Check::new(
CheckKind::NoAssertEquals,
Range::from_located(expr),
));
}
}
}
@@ -723,6 +773,88 @@ pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
}
}
// flakes8-comprehensions
/// Check `list(generator)` compliance.
pub fn unnecessary_generator_list(expr: &Expr, func: &Expr, args: &Vec<Expr>) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" {
if let ExprKind::GeneratorExp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorList,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `set(generator)` compliance.
pub fn unnecessary_generator_set(expr: &Expr, func: &Expr, args: &Vec<Expr>) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
if let ExprKind::GeneratorExp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorList,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `set([...])` compliance.
pub fn unnecessary_list_comprehension_set(
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
if let ExprKind::ListComp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionSet,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `dict([...])` compliance.
pub fn unnecessary_list_comprehension_dict(
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "dict" {
if let ExprKind::ListComp { elt, .. } = &args[0].node {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionDict,
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
}
None
}
// flake8-super
/// Check that `super()` has no args
pub fn check_super_args(
@@ -791,11 +923,16 @@ pub fn check_super_args(
// flake8-print
/// Check whether a function call is a `print` or `pprint` invocation
pub fn check_print_call(expr: &Expr, func: &Expr) -> Option<Check> {
pub fn check_print_call(
expr: &Expr,
func: &Expr,
check_print: bool,
check_pprint: bool,
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "print" {
if check_print && id == "print" {
return Some(Check::new(CheckKind::PrintFound, Range::from_located(expr)));
} else if id == "pprint" {
} else if check_pprint && id == "pprint" {
return Some(Check::new(
CheckKind::PPrintFound,
Range::from_located(expr),
@@ -805,7 +942,7 @@ pub fn check_print_call(expr: &Expr, func: &Expr) -> Option<Check> {
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let ExprKind::Name { id, .. } = &value.node {
if id == "pprint" && attr == "pprint" {
if check_pprint && id == "pprint" && attr == "pprint" {
return Some(Check::new(
CheckKind::PPrintFound,
Range::from_located(expr),

View File

@@ -21,27 +21,32 @@ use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{checks, operations, visitor};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::plugins;
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::python::future::ALL_FEATURE_NAMES;
use crate::python::typing;
use crate::settings::Settings;
use crate::settings::{PythonVersion, Settings};
pub const GLOBAL_SCOPE_INDEX: usize = 0;
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
struct Checker<'a> {
pub struct Checker<'a> {
// Input data.
locator: SourceCodeLocator<'a>,
settings: &'a Settings,
autofix: &'a fixer::Mode,
path: &'a Path,
// TODO(charlie): Separate immutable from mutable state. (None of these should ever change.)
pub(crate) locator: SourceCodeLocator<'a>,
pub(crate) settings: &'a Settings,
pub(crate) autofix: &'a fixer::Mode,
// Computed checks.
checks: Vec<Check>,
// Edit tracking.
// TODO(charlie): Instead of exposing deletions, wrap in a public API.
pub(crate) deletions: BTreeSet<usize>,
// Retain all scopes and parent nodes, along with a stack of indexes to track which are active
// at various points in time.
parents: Vec<&'a Stmt>,
parent_stack: Vec<usize>,
pub(crate) parents: Vec<&'a Stmt>,
pub(crate) parent_stack: Vec<usize>,
scopes: Vec<Scope>,
scope_stack: Vec<usize>,
dead_scopes: Vec<usize>,
@@ -50,7 +55,7 @@ struct Checker<'a> {
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>)>,
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_assignments: Vec<usize>,
// Derivative state.
// Internal, derivative state.
in_f_string: Option<Range>,
in_annotation: bool,
in_literal: bool,
@@ -58,8 +63,6 @@ struct Checker<'a> {
seen_docstring: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
// Edit tracking.
deletions: BTreeSet<usize>,
}
impl<'a> Checker<'a> {
@@ -105,11 +108,32 @@ fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
}
}
fn is_annotated_subscript(expr: &Expr) -> bool {
enum SubscriptKind {
AnnotatedSubscript,
PEP593AnnotatedSubscript,
}
fn match_annotated_subscript(expr: &Expr) -> Option<SubscriptKind> {
match &expr.node {
ExprKind::Attribute { attr, .. } => typing::is_annotated_subscript(attr),
ExprKind::Name { id, .. } => typing::is_annotated_subscript(id),
_ => false,
ExprKind::Attribute { attr, .. } => {
if typing::is_annotated_subscript(attr) {
Some(SubscriptKind::AnnotatedSubscript)
} else if typing::is_pep593_annotated_subscript(attr) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
None
}
}
ExprKind::Name { id, .. } => {
if typing::is_annotated_subscript(id) {
Some(SubscriptKind::AnnotatedSubscript)
} else if typing::is_pep593_annotated_subscript(id) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
None
}
}
_ => None,
}
}
@@ -216,8 +240,7 @@ where
StmtKind::Global { names } | StmtKind::Nonlocal { names } => {
let global_scope_id = self.scopes[GLOBAL_SCOPE_INDEX].id;
let current_scope =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
let current_scope = self.current_scope();
let current_scope_id = current_scope.id;
if current_scope_id != global_scope_id {
for name in names {
@@ -365,20 +388,8 @@ where
decorator_list,
..
} => {
if self.settings.enabled.contains(&CheckCode::R001) {
let scope =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(check) = checks::check_useless_object_inheritance(
stmt,
name,
bases,
keywords,
scope,
&mut self.locator,
self.autofix,
) {
self.checks.push(check);
}
if self.settings.enabled.contains(&CheckCode::U004) {
plugins::useless_object_inheritance(self, stmt, name, bases, keywords);
}
if self.settings.enabled.contains(&CheckCode::E742) {
@@ -597,25 +608,12 @@ where
}
StmtKind::If { test, .. } => {
if self.settings.enabled.contains(&CheckCode::F634) {
if let Some(check) =
checks::check_if_tuple(test, self.locate_check(Range::from_located(stmt)))
{
self.checks.push(check);
}
plugins::if_tuple(self, stmt, test);
}
}
StmtKind::Assert { test, .. } => {
if self
.settings
.enabled
.contains(CheckKind::AssertTuple.code())
{
if let Some(check) = checks::check_assert_tuple(
test,
self.locate_check(Range::from_located(stmt)),
) {
self.checks.push(check);
}
if self.settings.enabled.contains(&CheckCode::F631) {
plugins::assert_tuple(self, stmt, test);
}
}
StmtKind::Try { handlers, .. } => {
@@ -635,35 +633,7 @@ where
}
}
if self.settings.enabled.contains(&CheckCode::U001) {
if let Some(mut check) = checks::check_useless_metaclass_type(
targets,
value,
self.locate_check(Range::from_located(stmt)),
) {
if matches!(self.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let context = self.binding_context();
let deleted: Vec<&Stmt> = self
.deletions
.iter()
.map(|index| self.parents[*index])
.collect();
match fixes::remove_stmt(
self.parents[context.defined_by],
context.defined_in.map(|index| self.parents[index]),
&deleted,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
self.deletions.insert(context.defined_by);
}
check.amend(fix)
}
Err(e) => error!("Failed to fix unused imports: {}", e),
}
}
self.checks.push(check);
}
plugins::useless_metaclass_type(self, stmt, value, targets);
}
}
StmtKind::AnnAssign { value, .. } => {
@@ -774,81 +744,65 @@ where
self.check_builtin_shadowing(id, Range::from_located(expr), true);
let parent =
self.parents[*(self.parent_stack.last().expect("No parent found."))];
self.handle_node_store(expr, parent);
self.handle_node_store(expr, self.current_parent());
}
ExprContext::Del => self.handle_node_delete(expr),
},
ExprKind::Call { func, args, .. } => {
if self.settings.enabled.contains(&CheckCode::R002) {
if let Some(check) = checks::check_assert_equals(func, self.autofix) {
self.checks.push(check)
}
if self.settings.enabled.contains(&CheckCode::U005) {
plugins::assert_equals(self, func);
}
// flake8-super
if self.settings.enabled.contains(&CheckCode::SPR001) {
// Only bother going through the super check at all if we're in a `super` call.
// (We check this in `check_super_args` too, so this is just an optimization.)
if checks::is_super_call_with_arguments(func, args) {
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
let parents: Vec<&Stmt> = self
.parent_stack
.iter()
.map(|index| self.parents[*index])
.collect();
if let Some(mut check) =
checks::check_super_args(scope, &parents, expr, func, args)
{
if matches!(self.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) =
fixes::remove_super_arguments(&mut self.locator, expr)
{
check.amend(fix);
}
}
self.checks.push(check)
}
}
plugins::super_call_with_parameters(self, expr, func, args);
}
// flake8-print
if self.settings.enabled.contains(&CheckCode::T201)
|| self.settings.enabled.contains(&CheckCode::T203)
{
if let Some(mut check) = checks::check_print_call(expr, func) {
if matches!(self.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let context = self.binding_context();
if matches!(
self.parents[context.defined_by].node,
StmtKind::Expr { .. }
) {
let deleted: Vec<&Stmt> = self
.deletions
.iter()
.map(|index| self.parents[*index])
.collect();
plugins::print_call(self, expr, func);
}
match fixes::remove_stmt(
self.parents[context.defined_by],
context.defined_in.map(|index| self.parents[index]),
&deleted,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
self.deletions.insert(context.defined_by);
}
check.amend(fix)
}
Err(e) => error!("Failed to fix unused imports: {}", e),
}
}
}
// flake8-comprehensions
if self.settings.enabled.contains(&CheckCode::C400) {
if let Some(check) = checks::unnecessary_generator_list(expr, func, args) {
self.checks.push(check);
};
}
self.checks.push(check)
}
if self.settings.enabled.contains(&CheckCode::C401) {
if let Some(check) = checks::unnecessary_generator_set(expr, func, args) {
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C403) {
if let Some(check) =
checks::unnecessary_list_comprehension_set(expr, func, args)
{
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C404) {
if let Some(check) =
checks::unnecessary_list_comprehension_dict(expr, func, args)
{
self.checks.push(check);
};
}
// pyupgrade
if self.settings.enabled.contains(&CheckCode::U002)
&& self.settings.target_version >= PythonVersion::Py310
{
plugins::unnecessary_abspath(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::U003) {
plugins::type_of_primitive(self, expr, func, args);
}
if let ExprKind::Name { id, ctx } = &func.node {
@@ -879,8 +833,7 @@ where
}
}
ExprKind::Yield { .. } | ExprKind::YieldFrom { .. } | ExprKind::Await { .. } => {
let scope =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
let scope = self.current_scope();
if self
.settings
.enabled
@@ -916,22 +869,7 @@ where
..
} => {
if self.settings.enabled.contains(&CheckCode::F633) {
if let ExprKind::Name { id, .. } = &left.node {
if id == "print" {
let scope = &self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(Binding {
kind: BindingKind::Builtin,
..
}) = scope.values.get("print")
{
self.checks.push(Check::new(
CheckKind::InvalidPrintSyntax,
Range::from_located(left),
));
}
}
}
plugins::invalid_print_syntax(self, left);
}
}
ExprKind::UnaryOp { op, operand } => {
@@ -985,9 +923,11 @@ where
ExprKind::Constant {
value: Constant::Str(value),
..
} if self.in_annotation && !self.in_literal => {
self.deferred_string_annotations
.push((Range::from_located(expr), value));
} => {
if self.in_annotation && !self.in_literal {
self.deferred_string_annotations
.push((Range::from_located(expr), value));
}
}
ExprKind::Lambda { args, .. } => {
// Visit the arguments, but avoid the body, which will be deferred.
@@ -1138,12 +1078,35 @@ where
}
}
ExprKind::Subscript { value, slice, ctx } => {
if is_annotated_subscript(value) {
self.visit_expr(value);
self.visit_annotation(slice);
self.visit_expr_context(ctx);
} else {
visitor::walk_expr(self, expr);
match match_annotated_subscript(value) {
Some(subscript) => match subscript {
// Ex) Optional[int]
SubscriptKind::AnnotatedSubscript => {
self.visit_expr(value);
self.visit_annotation(slice);
self.visit_expr_context(ctx);
}
// Ex) Annotated[int, "Hello, world!"]
SubscriptKind::PEP593AnnotatedSubscript => {
// First argument is a type (including forward references); the rest are
// arbitrary Python objects.
self.visit_expr(value);
if let ExprKind::Tuple { elts, ctx } = &slice.node {
if let Some(expr) = elts.first() {
self.visit_expr(expr);
self.in_annotation = false;
for expr in elts.iter().skip(1) {
self.visit_expr(expr);
}
self.in_annotation = true;
self.visit_expr_context(ctx);
}
} else {
error!("Found non-ExprKind::Tuple argument to PEP 593 Annotation.")
}
}
},
None => visitor::walk_expr(self, expr),
}
}
_ => visitor::walk_expr(self, expr),
@@ -1191,11 +1154,7 @@ where
false,
);
let scope = &self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
if scope.values.contains_key(name) {
let parent = self.parents
[*(self.parent_stack.last().expect("No parent found."))];
if self.current_scope().values.contains_key(name) {
self.handle_node_store(
&Expr::new(
excepthandler.location,
@@ -1205,15 +1164,11 @@ where
ctx: ExprContext::Store,
},
),
parent,
self.current_parent(),
);
}
let parent =
self.parents[*(self.parent_stack.last().expect("No parent found."))];
let scope = &self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
let definition = scope.values.get(name).cloned();
let definition = self.current_scope().values.get(name).cloned();
self.handle_node_store(
&Expr::new(
excepthandler.location,
@@ -1223,7 +1178,7 @@ where
ctx: ExprContext::Store,
},
),
parent,
self.current_parent(),
);
walk_excepthandler(self, excepthandler);
@@ -1306,6 +1261,10 @@ impl CheckLocator for Checker<'_> {
}
impl<'a> Checker<'a> {
pub fn add_check(&mut self, check: Check) {
self.checks.push(check);
}
fn push_parent(&mut self, parent: &'a Stmt) {
self.parent_stack.push(self.parents.len());
self.parents.push(parent);
@@ -1355,7 +1314,15 @@ impl<'a> Checker<'a> {
}
}
fn binding_context(&self) -> BindingContext {
pub fn current_scope(&self) -> &Scope {
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))]
}
pub fn current_parent(&self) -> &'a Stmt {
self.parents[*(self.parent_stack.last().expect("No parent found."))]
}
pub fn binding_context(&self) -> BindingContext {
let mut rev = self.parent_stack.iter().rev().fuse();
let defined_by = *rev.next().expect("Expected to bind within a statement.");
let defined_in = rev.next().cloned();
@@ -1735,7 +1702,7 @@ impl<'a> Checker<'a> {
context.defined_by,
context.defined_in,
))
.or_insert(vec![]);
.or_default();
full_names.push(full_name);
}
BindingKind::Importation(full_name, context)
@@ -1746,7 +1713,7 @@ impl<'a> Checker<'a> {
context.defined_by,
context.defined_in,
))
.or_insert(vec![]);
.or_default();
full_names.push(full_name);
}
_ => {}
@@ -1758,12 +1725,8 @@ impl<'a> Checker<'a> {
let child = self.parents[defined_by];
let parent = defined_in.map(|defined_in| self.parents[defined_in]);
let mut check = Check::new(
CheckKind::UnusedImport(full_names.join(", ")),
self.locate_check(Range::from_located(child)),
);
if matches!(self.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let fix = if matches!(self.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
{
let deleted: Vec<&Stmt> = self
.deletions
.iter()
@@ -1776,14 +1739,22 @@ impl<'a> Checker<'a> {
};
match removal_fn(&mut self.locator, &full_names, child, parent, &deleted) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
self.deletions.insert(defined_by);
}
check.amend(fix)
Ok(fix) => Some(fix),
Err(e) => {
error!("Failed to fix unused imports: {}", e);
None
}
Err(e) => error!("Failed to fix unused imports: {}", e),
}
} else {
None
};
let mut check = Check::new(
CheckKind::UnusedImport(full_names.into_iter().map(String::from).collect()),
self.locate_check(Range::from_located(child)),
);
if let Some(fix) = fix {
check.amend(fix);
}
self.checks.push(check);
@@ -1793,7 +1764,7 @@ impl<'a> Checker<'a> {
}
fn check_builtin_shadowing(&mut self, name: &str, location: Range, is_attribute: bool) {
let scope = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
let scope = self.current_scope();
// flake8-builtins
if is_attribute && matches!(scope.kind, ScopeKind::Class) {

View File

@@ -1,11 +1,13 @@
use std::str::FromStr;
use crate::ast::types::Range;
use crate::ast::checks::Primitive;
use anyhow::Result;
use itertools::Itertools;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use crate::ast::types::Range;
pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [
// pycodestyle
CheckCode::E402,
@@ -53,7 +55,7 @@ pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [
CheckCode::F901,
];
pub const ALL_CHECK_CODES: [CheckCode; 52] = [
pub const ALL_CHECK_CODES: [CheckCode; 58] = [
// pycodestyle
CheckCode::E402,
CheckCode::E501,
@@ -102,6 +104,11 @@ pub const ALL_CHECK_CODES: [CheckCode; 52] = [
CheckCode::A001,
CheckCode::A002,
CheckCode::A003,
// flake8-comprehensions
CheckCode::C400,
CheckCode::C401,
CheckCode::C403,
CheckCode::C404,
// flake8-super
CheckCode::SPR001,
// flake8-print
@@ -109,9 +116,10 @@ pub const ALL_CHECK_CODES: [CheckCode; 52] = [
CheckCode::T203,
// pyupgrade
CheckCode::U001,
// Refactor
CheckCode::R001,
CheckCode::R002,
CheckCode::U002,
CheckCode::U003,
CheckCode::U004,
CheckCode::U005,
// Meta
CheckCode::M001,
];
@@ -166,6 +174,11 @@ pub enum CheckCode {
A001,
A002,
A003,
// flake8-comprehensions
C400,
C401,
C403,
C404,
// flake8-super
SPR001,
// flake8-print
@@ -173,9 +186,10 @@ pub enum CheckCode {
T203,
// pyupgrade
U001,
// Refactor
R001,
R002,
U002,
U003,
U004,
U005,
// Meta
M001,
}
@@ -233,13 +247,20 @@ impl FromStr for CheckCode {
"A001" => Ok(CheckCode::A001),
"A002" => Ok(CheckCode::A002),
"A003" => Ok(CheckCode::A003),
// flake8-comprehensions
"C403" => Ok(CheckCode::C403),
"C404" => Ok(CheckCode::C404),
// flake8-super
"SPR001" => Ok(CheckCode::SPR001),
// flake8-print
"T201" => Ok(CheckCode::T201),
"T203" => Ok(CheckCode::T203),
// pyupgrade
"U001" => Ok(CheckCode::U001),
// Refactor
"R001" => Ok(CheckCode::R001),
"R002" => Ok(CheckCode::R002),
"U002" => Ok(CheckCode::U002),
"U003" => Ok(CheckCode::U003),
"U004" => Ok(CheckCode::U004),
"U005" => Ok(CheckCode::U005),
// Meta
"M001" => Ok(CheckCode::M001),
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
@@ -298,6 +319,11 @@ impl CheckCode {
CheckCode::A001 => "A001",
CheckCode::A002 => "A002",
CheckCode::A003 => "A003",
// flake8-comprehensions
CheckCode::C400 => "C400",
CheckCode::C401 => "C401",
CheckCode::C403 => "C403",
CheckCode::C404 => "C404",
// flake8-super
CheckCode::SPR001 => "SPR001",
// flake8-print
@@ -305,9 +331,10 @@ impl CheckCode {
CheckCode::T203 => "T203",
// pyupgrade
CheckCode::U001 => "U001",
// Refactor
CheckCode::R001 => "R001",
CheckCode::R002 => "R002",
CheckCode::U002 => "U002",
CheckCode::U003 => "U003",
CheckCode::U004 => "U004",
CheckCode::U005 => "U005",
// Meta
CheckCode::M001 => "M001",
}
@@ -341,7 +368,7 @@ impl CheckCode {
CheckCode::E902 => CheckKind::IOError("IOError: `...`".to_string()),
CheckCode::E999 => CheckKind::SyntaxError("`...`".to_string()),
// pyflakes
CheckCode::F401 => CheckKind::UnusedImport("...".to_string()),
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()]),
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
CheckCode::F404 => CheckKind::LateFutureImport,
@@ -375,6 +402,11 @@ impl CheckCode {
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
CheckCode::A003 => CheckKind::BuiltinAttributeShadowing("...".to_string()),
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
CheckCode::C403 => CheckKind::UnnecessaryListComprehensionSet,
CheckCode::C404 => CheckKind::UnnecessaryListComprehensionDict,
// flake8-super
CheckCode::SPR001 => CheckKind::SuperCallWithParameters,
// flake8-print
@@ -382,9 +414,10 @@ impl CheckCode {
CheckCode::T203 => CheckKind::PPrintFound,
// pyupgrade
CheckCode::U001 => CheckKind::UselessMetaclassType,
// Refactor
CheckCode::R001 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::R002 => CheckKind::NoAssertEquals,
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
CheckCode::U003 => CheckKind::TypeOfPrimitive(Primitive::Str),
CheckCode::U004 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::U005 => CheckKind::NoAssertEquals,
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
@@ -433,7 +466,6 @@ pub enum CheckKind {
ModuleImportNotAtTopOfFile,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable(String),
NoAssertEquals,
NoneComparison(RejectedCmpop),
NotInTest,
NotIsTest,
@@ -446,21 +478,31 @@ pub enum CheckKind {
UndefinedExport(String),
UndefinedLocal(String),
UndefinedName(String),
UnusedImport(String),
UnusedNOQA(Option<String>),
UnusedImport(Vec<String>),
UnusedVariable(String),
UselessMetaclassType,
UselessObjectInheritance(String),
YieldOutsideFunction,
// flake8-builtin
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
// flakes8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
UnnecessaryListComprehensionSet,
UnnecessaryListComprehensionDict,
// flake8-super
SuperCallWithParameters,
// flake8-print
PrintFound,
PPrintFound,
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
UselessMetaclassType,
NoAssertEquals,
UselessObjectInheritance(String),
// Meta
UnusedNOQA(Option<String>),
}
impl CheckKind {
@@ -513,14 +555,20 @@ impl CheckKind {
CheckKind::BuiltinVariableShadowing(_) => "BuiltinVariableShadowing",
CheckKind::BuiltinArgumentShadowing(_) => "BuiltinArgumentShadowing",
CheckKind::BuiltinAttributeShadowing(_) => "BuiltinAttributeShadowing",
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => "UnnecessaryGeneratorList",
CheckKind::UnnecessaryGeneratorSet => "UnnecessaryGeneratorSet",
CheckKind::UnnecessaryListComprehensionSet => "UnnecessaryListComprehensionSet",
CheckKind::UnnecessaryListComprehensionDict => "UnnecessaryListComprehensionDict",
// flake8-super
CheckKind::SuperCallWithParameters => "SuperCallWithParameters",
// flake8-print
CheckKind::PrintFound => "PrintFound",
CheckKind::PPrintFound => "PPrintFound",
// pyupgrade
CheckKind::TypeOfPrimitive(_) => "TypeOfPrimitive",
CheckKind::UnnecessaryAbspath => "UnnecessaryAbspath",
CheckKind::UselessMetaclassType => "UselessMetaclassType",
// Refactor
CheckKind::NoAssertEquals => "NoAssertEquals",
CheckKind::UselessObjectInheritance(_) => "UselessObjectInheritance",
// Meta
@@ -577,16 +625,22 @@ impl CheckKind {
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
CheckKind::UnnecessaryGeneratorSet => &CheckCode::C401,
CheckKind::UnnecessaryListComprehensionSet => &CheckCode::C403,
CheckKind::UnnecessaryListComprehensionDict => &CheckCode::C404,
// flake8-super
CheckKind::SuperCallWithParameters => &CheckCode::SPR001,
// flake8-print
CheckKind::PrintFound => &CheckCode::T201,
CheckKind::PPrintFound => &CheckCode::T203,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
CheckKind::UselessMetaclassType => &CheckCode::U001,
// Refactor
CheckKind::NoAssertEquals => &CheckCode::R002,
CheckKind::UselessObjectInheritance(_) => &CheckCode::R001,
CheckKind::NoAssertEquals => &CheckCode::U005,
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
// Meta
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
}
@@ -712,7 +766,10 @@ impl CheckKind {
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UnusedImport(name) => format!("`{name}` imported but unused"),
CheckKind::UnusedImport(names) => {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("{names} imported but unused")
}
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
}
@@ -730,6 +787,19 @@ impl CheckKind {
CheckKind::BuiltinAttributeShadowing(name) => {
format!("Class attribute `{name}` is shadowing a python builtin")
}
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => {
"Unnecessary generator - rewrite as a list comprehension".to_string()
}
CheckKind::UnnecessaryGeneratorSet => {
"Unnecessary generator - rewrite as a set comprehension".to_string()
}
CheckKind::UnnecessaryListComprehensionSet => {
"Unnecessary list comprehension - rewrite as a set comprehension".to_string()
}
CheckKind::UnnecessaryListComprehensionDict => {
"Unnecessary list comprehension - rewrite as a dict comprehension".to_string()
}
// flake8-super
CheckKind::SuperCallWithParameters => {
"Use `super()` instead of `super(__class__, self)`".to_string()
@@ -738,8 +808,13 @@ impl CheckKind {
CheckKind::PrintFound => "`print` found".to_string(),
CheckKind::PPrintFound => "`pprint` found".to_string(),
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
}
CheckKind::UnnecessaryAbspath => {
"`abspath(__file__)` is unnecessary in Python 3.9 and later".to_string()
}
CheckKind::UselessMetaclassType => "`__metaclass__ = type` is implied".to_string(),
// Refactor
CheckKind::NoAssertEquals => {
"`assertEquals` is deprecated, use `assertEqual` instead".to_string()
}
@@ -762,6 +837,8 @@ impl CheckKind {
| CheckKind::PPrintFound
| CheckKind::PrintFound
| CheckKind::SuperCallWithParameters
| CheckKind::TypeOfPrimitive(_)
| CheckKind::UnnecessaryAbspath
| CheckKind::UnusedImport(_)
| CheckKind::UnusedNOQA(_)
| CheckKind::UselessMetaclassType

136
src/cli.rs Normal file
View File

@@ -0,0 +1,136 @@
use std::fmt;
use std::path::PathBuf;
use clap::{command, Parser};
use log::warn;
use regex::Regex;
use crate::checks::CheckCode;
use crate::printer::SerializationFormat;
use crate::pyproject::StrCheckCodePair;
use crate::settings::PythonVersion;
use crate::RawSettings;
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
#[command(version)]
pub struct Cli {
#[arg(required = true)]
pub files: Vec<PathBuf>,
/// Enable verbose logging.
#[arg(short, long)]
pub verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long)]
pub quiet: bool,
/// Exit with status code "0", even upon detecting errors.
#[arg(short, long)]
pub exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Attempt to automatically fix lint errors.
#[arg(short, long)]
pub fix: bool,
/// Disable cache reads.
#[arg(short, long)]
pub no_cache: bool,
/// List of error codes to enable.
#[arg(long, value_delimiter = ',')]
pub select: Vec<CheckCode>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')]
pub extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[arg(long, value_delimiter = ',')]
pub ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')]
pub extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
pub exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[arg(long, value_delimiter = ',')]
pub extend_exclude: Vec<String>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Vec<StrCheckCodePair>,
/// Output serialization format for error messages.
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
pub format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
/// See ruff's settings.
#[arg(long)]
pub show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[arg(long)]
pub add_noqa: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
pub dummy_variable_rgx: Option<Regex>,
/// The minimum Python version that should be supported.
#[arg(long)]
pub target_version: Option<PythonVersion>,
/// Round-trip auto-formatting.
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
pub autoformat: bool,
}
pub enum Warnable {
Select,
ExtendSelect,
}
impl fmt::Display for Warnable {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Warnable::Select => fmt.write_str("--select"),
Warnable::ExtendSelect => fmt.write_str("--extend-select"),
}
}
}
/// Warn the user if they attempt to enable a code that won't be respected.
pub fn warn_on(
flag: Warnable,
codes: &Vec<CheckCode>,
cli_ignore: &Vec<CheckCode>,
cli_extend_ignore: &Vec<CheckCode>,
pyproject_settings: &RawSettings,
pyproject_path: &Option<PathBuf>,
) {
for code in codes {
if !cli_ignore.is_empty() {
if cli_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --ignore")
}
} else if pyproject_settings.ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `ignore` field in {}",
path.to_string_lossy()
)
} else {
warn!("{code:?} was passed to {flag}, but ignored by the default `ignore` field",)
}
}
if !cli_extend_ignore.is_empty() {
if cli_extend_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --extend-ignore")
}
} else if pyproject_settings.extend_ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}",
path.to_string_lossy()
)
} else {
warn!("{code:?} was passed to {flag}, but ignored by the default `extend_ignore` field")
}
}
}
}

View File

@@ -15,12 +15,14 @@ pub mod cache;
pub mod check_ast;
mod check_lines;
pub mod checks;
pub mod cli;
pub mod code_gen;
pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
mod noqa;
mod plugins;
pub mod printer;
pub mod pyproject;
mod python;
@@ -40,7 +42,7 @@ pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_raw(RawSettings::from_pyproject(pyproject, project_root)?);
let settings = Settings::from_raw(RawSettings::from_pyproject(&pyproject, &project_root)?);
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -690,30 +690,6 @@ mod tests {
Ok(())
}
#[test]
fn r001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R001.py"),
&settings::Settings::for_rule(CheckCode::R001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn r002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R002.py"),
&settings::Settings::for_rule(CheckCode::R002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
@@ -786,6 +762,54 @@ mod tests {
Ok(())
}
#[test]
fn c400() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C400.py"),
&settings::Settings::for_rule(CheckCode::C400),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c401() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C401.py"),
&settings::Settings::for_rule(CheckCode::C401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c403() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C403.py"),
&settings::Settings::for_rule(CheckCode::C403),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C404.py"),
&settings::Settings::for_rule(CheckCode::C404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn spr001() -> Result<()> {
let mut checks = check_path(
@@ -833,4 +857,52 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U002.py"),
&settings::Settings::for_rule(CheckCode::U002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u003() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U003.py"),
&settings::Settings::for_rule(CheckCode::U003),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u004() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U004.py"),
&settings::Settings::for_rule(CheckCode::U004),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u005() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U005.py"),
&settings::Settings::for_rule(CheckCode::U005),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -5,98 +5,33 @@ use std::sync::mpsc::channel;
use std::time::Instant;
use anyhow::Result;
use clap::{command, Parser};
use clap::Parser;
use colored::Colorize;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use regex::Regex;
use walkdir::DirEntry;
use ::ruff::cache;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::add_noqa_to_path;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::pyproject::{self, StrCheckCodePair};
use ::ruff::settings::CurrentSettings;
use ::ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ::ruff::tell_user;
use ruff::cache;
use ruff::checks::CheckCode;
use ruff::checks::CheckKind;
use ruff::cli::{warn_on, Cli, Warnable};
use ruff::fs::iter_python_files;
use ruff::linter::add_noqa_to_path;
use ruff::linter::autoformat_path;
use ruff::linter::lint_path;
use ruff::logging::set_up_logging;
use ruff::message::Message;
use ruff::printer::{Printer, SerializationFormat};
use ruff::pyproject::{self};
use ruff::settings::CurrentSettings;
use ruff::settings::RawSettings;
use ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ruff::tell_user;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
#[command(version)]
struct Cli {
#[arg(required = true)]
files: Vec<PathBuf>,
/// Enable verbose logging.
#[arg(short, long)]
verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long)]
quiet: bool,
/// Exit with status code "0", even upon detecting errors.
#[arg(short, long)]
exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
watch: bool,
/// Attempt to automatically fix lint errors.
#[arg(short, long)]
fix: bool,
/// Disable cache reads.
#[arg(short, long)]
no_cache: bool,
/// List of error codes to enable.
#[arg(long, value_delimiter = ',')]
select: Vec<CheckCode>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')]
extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[arg(long, value_delimiter = ',')]
ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')]
extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[arg(long, value_delimiter = ',')]
extend_exclude: Vec<String>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
per_file_ignores: Vec<StrCheckCodePair>,
/// Output serialization format for error messages.
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[arg(long)]
show_files: bool,
/// See ruff's settings.
#[arg(long)]
show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[arg(long)]
add_noqa: bool,
/// Enable automatic formatting.
#[arg(long)]
autoformat: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
dummy_variable_rgx: Option<Regex>,
}
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
@@ -121,8 +56,11 @@ fn check_for_updates() {
}
}
fn show_settings(settings: RawSettings) {
println!("{:#?}", CurrentSettings::from_settings(settings));
fn show_settings(settings: RawSettings, project_root: Option<PathBuf>, pyproject: Option<PathBuf>) {
println!(
"{:#?}",
CurrentSettings::from_settings(settings, project_root, pyproject)
);
}
fn show_files(files: &[PathBuf], settings: &Settings) {
@@ -288,7 +226,7 @@ fn inner_main() -> Result<ExitCode> {
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect();
let mut settings = RawSettings::from_pyproject(pyproject, project_root)?;
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root)?;
if !exclude.is_empty() {
settings.exclude = exclude;
}
@@ -299,9 +237,25 @@ fn inner_main() -> Result<ExitCode> {
settings.per_file_ignores = per_file_ignores;
}
if !cli.select.is_empty() {
warn_on(
Warnable::Select,
&cli.select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&pyproject,
);
settings.select = cli.select;
}
if !cli.extend_select.is_empty() {
warn_on(
Warnable::ExtendSelect,
&cli.extend_select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&pyproject,
);
settings.extend_select = cli.extend_select;
}
if !cli.ignore.is_empty() {
@@ -310,6 +264,9 @@ fn inner_main() -> Result<ExitCode> {
if !cli.extend_ignore.is_empty() {
settings.extend_ignore = cli.extend_ignore;
}
if let Some(target_version) = cli.target_version {
settings.target_version = target_version;
}
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
settings.dummy_variable_rgx = dummy_variable_rgx;
}
@@ -319,7 +276,7 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(settings);
show_settings(settings, project_root, pyproject);
return Ok(ExitCode::SUCCESS);
}
@@ -403,7 +360,7 @@ fn inner_main() -> Result<ExitCode> {
#[cfg(feature = "update-informer")]
check_for_updates();
if !messages.is_empty() && !cli.exit_zero {
if messages.iter().any(|message| !message.fixed) && !cli.exit_zero {
return Ok(ExitCode::FAILURE);
}
}

View File

@@ -104,9 +104,7 @@ fn add_noqa_inner(
.unwrap_or(lineno);
if !codes.is_empty() {
let matches = matches_by_line
.entry(noqa_lineno)
.or_insert_with(BTreeSet::new);
let matches = matches_by_line.entry(noqa_lineno).or_default();
matches.append(&mut codes);
}
}

21
src/plugins.rs Normal file
View File

@@ -0,0 +1,21 @@
mod assert_equals;
mod assert_tuple;
mod if_tuple;
mod invalid_print_syntax;
mod print_call;
mod super_call_with_parameters;
mod type_of_primitive;
mod unnecessary_abspath;
mod useless_metaclass_type;
mod useless_object_inheritance;
pub use assert_equals::assert_equals;
pub use assert_tuple::assert_tuple;
pub use if_tuple::if_tuple;
pub use invalid_print_syntax::invalid_print_syntax;
pub use print_call::print_call;
pub use super_call_with_parameters::super_call_with_parameters;
pub use type_of_primitive::type_of_primitive;
pub use unnecessary_abspath::unnecessary_abspath;
pub use useless_metaclass_type::useless_metaclass_type;
pub use useless_object_inheritance::useless_object_inheritance;

View File

@@ -0,0 +1,23 @@
use rustpython_ast::{Expr, Location};
use crate::ast::checks;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::Fix;
pub fn assert_equals(checker: &mut Checker, expr: &Expr) {
if let Some(mut check) = checks::check_assert_equals(expr) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "assertEqual".to_string(),
location: Location::new(expr.location.row(), expr.location.column() + 1),
end_location: Location::new(
expr.location.row(),
expr.location.column() + 1 + "assertEquals".len(),
),
applied: false,
});
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,13 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::checks;
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
pub fn assert_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if let Some(check) =
checks::check_assert_tuple(test, checker.locate_check(Range::from_located(stmt)))
{
checker.add_check(check);
}
}

13
src/plugins/if_tuple.rs Normal file
View File

@@ -0,0 +1,13 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::checks;
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
pub fn if_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if let Some(check) =
checks::check_if_tuple(test, checker.locate_check(Range::from_located(stmt)))
{
checker.add_check(check);
}
}

View File

@@ -0,0 +1,23 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::{Binding, BindingKind, Range};
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
pub fn invalid_print_syntax(checker: &mut Checker, left: &Expr) {
if let ExprKind::Name { id, .. } = &left.node {
if id == "print" {
let scope = checker.current_scope();
if let Some(Binding {
kind: BindingKind::Builtin,
..
}) = scope.values.get("print")
{
checker.add_check(Check::new(
CheckKind::InvalidPrintSyntax,
Range::from_located(left),
));
}
}
}
}

46
src/plugins/print_call.rs Normal file
View File

@@ -0,0 +1,46 @@
use log::error;
use rustpython_ast::{Expr, Stmt, StmtKind};
use crate::ast::checks;
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
use crate::checks::CheckCode;
pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
if let Some(mut check) = checks::check_print_call(
expr,
func,
checker.settings.enabled.contains(&CheckCode::T201),
checker.settings.enabled.contains(&CheckCode::T203),
) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let context = checker.binding_context();
if matches!(
checker.parents[context.defined_by].node,
StmtKind::Expr { .. }
) {
let deleted: Vec<&Stmt> = checker
.deletions
.iter()
.map(|index| checker.parents[*index])
.collect();
match fixes::remove_stmt(
checker.parents[context.defined_by],
context.defined_in.map(|index| checker.parents[index]),
&deleted,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
checker.deletions.insert(context.defined_by);
}
check.amend(fix)
}
Err(e) => error!("Failed to fix unused imports: {}", e),
}
}
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,31 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::checks;
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
pub fn super_call_with_parameters(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) {
// Only bother going through the super check at all if we're in a `super` call.
// (We check this in `check_super_args` too, so this is just an optimization.)
if checks::is_super_call_with_arguments(func, args) {
let scope = checker.current_scope();
let parents: Vec<&Stmt> = checker
.parent_stack
.iter()
.map(|index| checker.parents[*index])
.collect();
if let Some(mut check) = checks::check_super_args(scope, &parents, expr, func, args) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_super_arguments(&mut checker.locator, expr) {
check.amend(fix);
}
}
checker.add_check(check)
}
}
}

View File

@@ -0,0 +1,25 @@
use rustpython_ast::Expr;
use crate::ast::checks;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{CheckKind, Fix};
pub fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, args: &Vec<Expr>) {
if let Some(mut check) =
checks::check_type_of_primitive(func, args, checker.locate_check(Range::from_located(expr)))
{
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let CheckKind::TypeOfPrimitive(primitive) = &check.kind {
check.amend(Fix {
content: primitive.builtin(),
location: expr.location,
end_location: expr.end_location,
applied: false,
});
}
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,25 @@
use rustpython_ast::Expr;
use crate::ast::checks;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::Fix;
pub fn unnecessary_abspath(checker: &mut Checker, expr: &Expr, func: &Expr, args: &Vec<Expr>) {
if let Some(mut check) = checks::check_unnecessary_abspath(
func,
args,
checker.locate_check(Range::from_located(expr)),
) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "__file__".to_string(),
location: expr.location,
end_location: expr.end_location,
applied: false,
});
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,44 @@
use log::error;
use rustpython_ast::{Expr, Stmt};
use crate::ast::checks;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
pub fn useless_metaclass_type(
checker: &mut Checker,
stmt: &Stmt,
value: &Expr,
targets: &Vec<Expr>,
) {
if let Some(mut check) = checks::check_useless_metaclass_type(
targets,
value,
checker.locate_check(Range::from_located(stmt)),
) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let context = checker.binding_context();
let deleted: Vec<&Stmt> = checker
.deletions
.iter()
.map(|index| checker.parents[*index])
.collect();
match fixes::remove_stmt(
checker.parents[context.defined_by],
context.defined_in.map(|index| checker.parents[index]),
&deleted,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
checker.deletions.insert(context.defined_by);
}
check.amend(fix)
}
Err(e) => error!("Failed to fix unused imports: {}", e),
}
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,29 @@
use rustpython_ast::{Expr, Keyword, Stmt};
use crate::ast::checks;
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
pub fn useless_object_inheritance(
checker: &mut Checker,
stmt: &Stmt,
name: &str,
bases: &[Expr],
keywords: &[Keyword],
) {
let scope = checker.current_scope();
if let Some(mut check) = checks::check_useless_object_inheritance(name, bases, scope) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_class_def_base(
&mut checker.locator,
&stmt.location,
check.location,
bases,
keywords,
) {
check.amend(fix);
}
}
checker.add_check(check);
}
}

View File

@@ -9,6 +9,7 @@ use serde::{Deserialize, Deserializer};
use crate::checks::CheckCode;
use crate::fs;
use crate::settings::PythonVersion;
pub fn load_config(pyproject: &Option<PathBuf>) -> Result<Config> {
match pyproject {
@@ -41,6 +42,7 @@ pub struct Config {
#[serde(default)]
pub per_file_ignores: Vec<StrCheckCodePair>,
pub dummy_variable_rgx: Option<String>,
pub target_version: Option<PythonVersion>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -189,6 +191,7 @@ mod tests {
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -213,6 +216,7 @@ line-length = 79
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -237,6 +241,7 @@ exclude = ["foo.py"]
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -261,6 +266,7 @@ select = ["E501"]
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -286,6 +292,7 @@ ignore = ["E501"]
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -354,6 +361,7 @@ other-attribute = 1
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
}
);

View File

@@ -7,6 +7,7 @@ static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
"AbstractAsyncContextManager",
"AbstractContextManager",
"AbstractSet",
// "Annotated",
"AsyncContextManager",
"AsyncGenerator",
"AsyncIterable",
@@ -87,3 +88,7 @@ static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
pub fn is_annotated_subscript(name: &str) -> bool {
ANNOTATED_SUBSCRIPTS.contains(name)
}
pub fn is_pep593_annotated_subscript(name: &str) -> bool {
name == "Annotated"
}

View File

@@ -1,16 +1,50 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use glob::Pattern;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::checks::{CheckCode, DEFAULT_CHECK_CODES};
use crate::fs;
use crate::pyproject::{load_config, StrCheckCodePair};
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)]
pub enum PythonVersion {
Py33,
Py34,
Py35,
Py36,
Py37,
Py38,
Py39,
Py310,
Py311,
}
impl FromStr for PythonVersion {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"py33" => Ok(PythonVersion::Py33),
"py34" => Ok(PythonVersion::Py34),
"py35" => Ok(PythonVersion::Py35),
"py36" => Ok(PythonVersion::Py36),
"py37" => Ok(PythonVersion::Py37),
"py38" => Ok(PythonVersion::Py38),
"py39" => Ok(PythonVersion::Py39),
"py310" => Ok(PythonVersion::Py310),
"py311" => Ok(PythonVersion::Py311),
_ => Err(anyhow!("Unknown version: {}", string)),
}
}
}
#[derive(Debug, Clone, Hash)]
pub enum FilePattern {
Simple(&'static str),
@@ -60,9 +94,8 @@ pub struct RawSettings {
pub ignore: Vec<CheckCode>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
pub select: Vec<CheckCode>,
pub target_version: PythonVersion,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
@@ -94,44 +127,43 @@ static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
impl RawSettings {
pub fn from_pyproject(
pyproject: Option<PathBuf>,
project_root: Option<PathBuf>,
pyproject: &Option<PathBuf>,
project_root: &Option<PathBuf>,
) -> Result<Self> {
let config = load_config(&pyproject)?;
let config = load_config(pyproject)?;
Ok(RawSettings {
dummy_variable_rgx: match config.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
target_version: config.target_version.unwrap_or(PythonVersion::Py310),
exclude: config
.exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
extend_exclude: config
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.map(|path| FilePattern::from_user(path, project_root))
.collect(),
extend_ignore: config.extend_ignore,
select: config
.select
.unwrap_or_else(|| DEFAULT_CHECK_CODES.to_vec()),
extend_select: config.extend_select,
ignore: config.ignore,
line_length: config.line_length.unwrap_or(88),
per_file_ignores: config
.per_file_ignores
.into_iter()
.map(|pair| PerFileIgnore::new(pair, &project_root))
.map(|pair| PerFileIgnore::new(pair, project_root))
.collect(),
project_root,
pyproject,
select: config
.select
.unwrap_or_else(|| DEFAULT_CHECK_CODES.to_vec()),
})
}
}
@@ -144,6 +176,7 @@ pub struct Settings {
pub extend_exclude: Vec<FilePattern>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub target_version: PythonVersion,
}
impl Settings {
@@ -165,6 +198,7 @@ impl Settings {
extend_exclude: settings.extend_exclude,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
target_version: PythonVersion::Py310,
}
}
@@ -176,6 +210,7 @@ impl Settings {
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
}
}
@@ -187,6 +222,7 @@ impl Settings {
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
}
}
}
@@ -238,13 +274,18 @@ pub struct CurrentSettings {
pub ignore: Vec<CheckCode>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>,
pub target_version: PythonVersion,
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
pub select: Vec<CheckCode>,
}
impl CurrentSettings {
pub fn from_settings(settings: RawSettings) -> Self {
pub fn from_settings(
settings: RawSettings,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) -> Self {
Self {
dummy_variable_rgx: settings.dummy_variable_rgx,
exclude: settings
@@ -262,9 +303,10 @@ impl CurrentSettings {
ignore: settings.ignore,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
project_root: settings.project_root,
pyproject: settings.pyproject,
select: settings.select,
target_version: settings.target_version,
project_root,
pyproject,
}
}
}

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryGeneratorList
location:
row: 1
column: 5
end_location:
row: 1
column: 30
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryGeneratorList
location:
row: 1
column: 5
end_location:
row: 1
column: 29
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryListComprehensionSet
location:
row: 1
column: 5
end_location:
row: 1
column: 31
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryListComprehensionDict
location:
row: 1
column: 5
end_location:
row: 1
column: 37
fix: ~

View File

@@ -3,7 +3,8 @@ source: src/linter.rs
expression: checks
---
- kind:
UnusedImport: functools
UnusedImport:
- functools
location:
row: 2
column: 1
@@ -20,7 +21,8 @@ expression: checks
column: 21
applied: false
- kind:
UnusedImport: collections.OrderedDict
UnusedImport:
- collections.OrderedDict
location:
row: 4
column: 1
@@ -37,7 +39,8 @@ expression: checks
column: 2
applied: false
- kind:
UnusedImport: logging.handlers
UnusedImport:
- logging.handlers
location:
row: 12
column: 1
@@ -54,7 +57,8 @@ expression: checks
column: 24
applied: false
- kind:
UnusedImport: shelve
UnusedImport:
- shelve
location:
row: 33
column: 5
@@ -71,7 +75,8 @@ expression: checks
column: 1
applied: false
- kind:
UnusedImport: importlib
UnusedImport:
- importlib
location:
row: 34
column: 5
@@ -79,16 +84,17 @@ expression: checks
row: 34
column: 21
fix:
content: pass
content: ""
location:
row: 34
column: 5
column: 1
end_location:
row: 34
column: 21
row: 35
column: 1
applied: false
- kind:
UnusedImport: pathlib
UnusedImport:
- pathlib
location:
row: 38
column: 5
@@ -105,7 +111,8 @@ expression: checks
column: 1
applied: false
- kind:
UnusedImport: pickle
UnusedImport:
- pickle
location:
row: 53
column: 9

View File

@@ -74,4 +74,31 @@ expression: checks
row: 89
column: 9
fix: ~
- kind:
UndefinedName: PEP593Test123
location:
row: 114
column: 10
end_location:
row: 114
column: 24
fix: ~
- kind:
UndefinedName: foo
location:
row: 122
column: 15
end_location:
row: 122
column: 19
fix: ~
- kind:
UndefinedName: bar
location:
row: 122
column: 22
end_location:
row: 122
column: 26
fix: ~

View File

@@ -3,7 +3,8 @@ source: src/linter.rs
expression: checks
---
- kind:
UnusedImport: models.Nut
UnusedImport:
- models.Nut
location:
row: 5
column: 1

View File

@@ -0,0 +1,53 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryAbspath
location:
row: 3
column: 5
end_location:
row: 3
column: 22
fix:
content: __file__
location:
row: 3
column: 5
end_location:
row: 3
column: 22
applied: false
- kind: UnnecessaryAbspath
location:
row: 9
column: 5
end_location:
row: 9
column: 30
fix:
content: __file__
location:
row: 9
column: 5
end_location:
row: 9
column: 30
applied: false
- kind: UnnecessaryAbspath
location:
row: 15
column: 5
end_location:
row: 15
column: 27
fix:
content: __file__
location:
row: 15
column: 5
end_location:
row: 15
column: 27
applied: false

View File

@@ -0,0 +1,90 @@
---
source: src/linter.rs
expression: checks
---
- kind:
TypeOfPrimitive: Str
location:
row: 1
column: 1
end_location:
row: 1
column: 9
fix:
content: str
location:
row: 1
column: 1
end_location:
row: 1
column: 9
applied: false
- kind:
TypeOfPrimitive: Bytes
location:
row: 2
column: 1
end_location:
row: 2
column: 10
fix:
content: bytes
location:
row: 2
column: 1
end_location:
row: 2
column: 10
applied: false
- kind:
TypeOfPrimitive: Int
location:
row: 3
column: 1
end_location:
row: 3
column: 8
fix:
content: int
location:
row: 3
column: 1
end_location:
row: 3
column: 8
applied: false
- kind:
TypeOfPrimitive: Float
location:
row: 4
column: 1
end_location:
row: 4
column: 9
fix:
content: float
location:
row: 4
column: 1
end_location:
row: 4
column: 9
applied: false
- kind:
TypeOfPrimitive: Complex
location:
row: 5
column: 1
end_location:
row: 5
column: 9
fix:
content: complex
location:
row: 5
column: 1
end_location:
row: 5
column: 9
applied: false