Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ae3bfc94 | ||
|
|
62e6feadc7 | ||
|
|
18a26e8f0b | ||
|
|
2371de3895 | ||
|
|
e3c8f61340 | ||
|
|
f6628ae100 | ||
|
|
989ed9c10b | ||
|
|
8698c06c36 | ||
|
|
dfd8a4158d | ||
|
|
c247730bf5 | ||
|
|
024472d578 | ||
|
|
7d69a153e8 | ||
|
|
4fc68e0310 | ||
|
|
6a24351202 | ||
|
|
d7f95ac6b6 | ||
|
|
11234ea555 | ||
|
|
b536159541 |
@@ -1,5 +1,5 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff
|
||||
rev: v0.0.30
|
||||
rev: v0.0.33
|
||||
hooks:
|
||||
- id: lint
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -1744,7 +1744,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.30"
|
||||
version = "0.0.33"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@@ -1759,6 +1759,7 @@ dependencies = [
|
||||
"filetime",
|
||||
"glob",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"notify",
|
||||
"once_cell",
|
||||
@@ -1787,7 +1788,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-ast"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=8cc3d23ddaa6339bf56608adaeefabff3ad329e7#8cc3d23ddaa6339bf56608adaeefabff3ad329e7"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"rustpython-compiler-core",
|
||||
@@ -1796,7 +1797,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-compiler-core"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=8cc3d23ddaa6339bf56608adaeefabff3ad329e7#8cc3d23ddaa6339bf56608adaeefabff3ad329e7"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags",
|
||||
@@ -1813,7 +1814,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=8cc3d23ddaa6339bf56608adaeefabff3ad329e7#8cc3d23ddaa6339bf56608adaeefabff3ad329e7"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.30"
|
||||
version = "0.0.33"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -20,12 +20,13 @@ fern = { version = "0.6.1" }
|
||||
filetime = { version = "0.2.17" }
|
||||
glob = { version = "0.3.0" }
|
||||
itertools = "0.10.3"
|
||||
lazy_static = "1.4.0"
|
||||
log = { version = "0.4.17" }
|
||||
notify = { version = "4.0.17" }
|
||||
once_cell = { version = "1.13.1" }
|
||||
rayon = { version = "1.5.3" }
|
||||
regex = { version = "1.6.0" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "8cc3d23ddaa6339bf56608adaeefabff3ad329e7" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" }
|
||||
serde = { version = "1.0.143", features = ["derive"] }
|
||||
serde_json = { version = "1.0.83" }
|
||||
toml = { version = "0.5.9" }
|
||||
|
||||
31
README.md
31
README.md
@@ -57,7 +57,7 @@ ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on sys
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff
|
||||
rev: v0.0.30
|
||||
rev: v0.0.33
|
||||
hooks:
|
||||
- id: lint
|
||||
```
|
||||
@@ -86,7 +86,7 @@ ruff path/to/code/ --select F401 F403
|
||||
See `ruff --help` for more:
|
||||
|
||||
```shell
|
||||
ruff (v0.0.30)
|
||||
ruff (v0.0.33)
|
||||
An extremely fast Python linter.
|
||||
|
||||
USAGE:
|
||||
@@ -96,16 +96,17 @@ ARGS:
|
||||
<FILES>...
|
||||
|
||||
OPTIONS:
|
||||
-e, --exit-zero Exit with status code "0", even upon detecting errors
|
||||
-f, --fix Attempt to automatically fix lint errors
|
||||
-h, --help Print help information
|
||||
--ignore <IGNORE>... Comma-separated list of error codes to ignore
|
||||
-n, --no-cache Disable cache reads
|
||||
-q, --quiet Disable all logging (but still exit with status code "1" upon
|
||||
detecting errors)
|
||||
--select <SELECT>... Comma-separated list of error codes to enable
|
||||
-v, --verbose Enable verbose logging
|
||||
-w, --watch Run in watch mode by re-running whenever files change
|
||||
-e, --exit-zero Exit with status code "0", even upon detecting errors
|
||||
--exclude <EXCLUDE>... List of file and/or directory patterns to exclude from checks
|
||||
-f, --fix Attempt to automatically fix lint errors
|
||||
-h, --help Print help information
|
||||
--ignore <IGNORE>... List of error codes to ignore
|
||||
-n, --no-cache Disable cache reads
|
||||
-q, --quiet Disable all logging (but still exit with status code "1" upon
|
||||
detecting errors)
|
||||
--select <SELECT>... List of error codes to enable
|
||||
-v, --verbose Enable verbose logging
|
||||
-w, --watch Run in watch mode by re-running whenever files change
|
||||
```
|
||||
|
||||
### Compatibility with Black
|
||||
@@ -123,7 +124,7 @@ ruff's goal is to achieve feature-parity with Flake8 when used (1) without any p
|
||||
stylistic checks; limiting to Python 3 obviates the need for certain compatibility checks.)
|
||||
|
||||
Under those conditions, Flake8 implements about 58 rules, give or take. At time of writing, ruff
|
||||
implements 24 rules. (Note that these 24 rules likely cover a disproportionate share of errors:
|
||||
implements 28 rules. (Note that these 28 rules likely cover a disproportionate share of errors:
|
||||
unused imports, undefined variables, etc.)
|
||||
|
||||
Of the unimplemented rules, ruff is missing:
|
||||
@@ -158,6 +159,8 @@ Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis F
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
|
||||
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated |
|
||||
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated |
|
||||
| F621 | TooManyExpressionsInStarredAssignment | too many expressions in star-unpacking assignment |
|
||||
| F622 | TwoStarredExpressions | two starred expressions in assignment |
|
||||
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` |
|
||||
| F634 | IfTuple | If test is a tuple, which is always `True` |
|
||||
| F704 | YieldOutsideFunction | a `yield` or `yield from` statement outside of a function/method |
|
||||
@@ -204,7 +207,7 @@ git clone --branch 3.10 https://github.com/python/cpython.git resources/test/cpy
|
||||
Add this `pyproject.toml` to the CPython directory:
|
||||
|
||||
```toml
|
||||
[tool.linter]
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
exclude = [
|
||||
"Lib/lib2to3/tests/data/bom.py",
|
||||
|
||||
@@ -21,7 +21,9 @@ fn main() {
|
||||
CheckKind::NotIsTest,
|
||||
CheckKind::RaiseNotImplemented,
|
||||
CheckKind::ReturnOutsideFunction,
|
||||
CheckKind::TooManyExpressionsInStarredAssignment,
|
||||
CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
|
||||
CheckKind::TwoStarredExpressions,
|
||||
CheckKind::UndefinedExport("...".to_string()),
|
||||
CheckKind::UndefinedLocal("...".to_string()),
|
||||
CheckKind::UndefinedName("...".to_string()),
|
||||
|
||||
9
resources/test/fixtures/F401.py
vendored
9
resources/test/fixtures/F401.py
vendored
@@ -11,10 +11,13 @@ import multiprocessing.pool
|
||||
import multiprocessing.process
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
from typing import NamedTuple, Dict, Type, TypeVar, List, Set
|
||||
from typing import TYPING_CHECK, NamedTuple, Dict, Type, TypeVar, List, Set, Union, cast
|
||||
|
||||
from blah import ClassA, ClassB, ClassC
|
||||
|
||||
if TYPING_CHECK:
|
||||
from models import Fruit, Nut, Vegetable
|
||||
|
||||
|
||||
class X:
|
||||
datetime: datetime
|
||||
@@ -32,3 +35,7 @@ __all__ += ["ClassC"]
|
||||
X = TypeVar("X")
|
||||
Y = TypeVar("Y", bound="Dict")
|
||||
Z = TypeVar("Z", "List", "Set")
|
||||
|
||||
a = list["Fruit"]
|
||||
b = Union["Nut", None]
|
||||
c = cast("Vegetable", b)
|
||||
|
||||
3
resources/test/fixtures/F622.py
vendored
Normal file
3
resources/test/fixtures/F622.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*a, *b, c = (1, 2, 3)
|
||||
*a, b, c = (1, 2, 3)
|
||||
a, b, *c = (1, 2, 3)
|
||||
21
resources/test/fixtures/F821.py
vendored
21
resources/test/fixtures/F821.py
vendored
@@ -56,3 +56,24 @@ except Exception as e:
|
||||
y: int = 1
|
||||
|
||||
x: "Bar" = 1
|
||||
|
||||
[first] = ["yup"]
|
||||
|
||||
|
||||
from typing import List, TypedDict
|
||||
|
||||
|
||||
class Item(TypedDict):
|
||||
nodes: List[TypedDict("Node", {"name": str})]
|
||||
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Ticket:
|
||||
class Status(Enum):
|
||||
OPEN = "OPEN"
|
||||
CLOSED = "CLOSED"
|
||||
|
||||
def set_status(self, status: Status):
|
||||
self.status = status
|
||||
|
||||
2
resources/test/fixtures/pyproject.toml
vendored
2
resources/test/fixtures/pyproject.toml
vendored
@@ -15,6 +15,8 @@ select = [
|
||||
"F541",
|
||||
"F601",
|
||||
"F602",
|
||||
"F621",
|
||||
"F622",
|
||||
"F631",
|
||||
"F634",
|
||||
"F704",
|
||||
|
||||
5
src/ast.rs
Normal file
5
src/ast.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod checks;
|
||||
pub mod operations;
|
||||
pub mod relocate;
|
||||
pub mod types;
|
||||
pub mod visitor;
|
||||
430
src/ast/checks.rs
Normal file
430
src/ast/checks.rs
Normal file
@@ -0,0 +1,430 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::izip;
|
||||
use rustpython_parser::ast::{
|
||||
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword,
|
||||
Location, Stmt, Unaryop,
|
||||
};
|
||||
|
||||
use crate::ast::operations::SourceCodeLocator;
|
||||
use crate::ast::types::{Binding, BindingKind, Scope};
|
||||
use crate::autofix::{fixer, fixes};
|
||||
use crate::checks::{Check, CheckKind, Fix, RejectedCmpop};
|
||||
|
||||
/// Check IfTuple compliance.
|
||||
pub fn check_if_tuple(test: &Expr, location: Location) -> Option<Check> {
|
||||
if let ExprKind::Tuple { elts, .. } = &test.node {
|
||||
if !elts.is_empty() {
|
||||
return Some(Check::new(CheckKind::IfTuple, location));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check AssertTuple compliance.
|
||||
pub fn check_assert_tuple(test: &Expr, location: Location) -> Option<Check> {
|
||||
if let ExprKind::Tuple { elts, .. } = &test.node {
|
||||
if !elts.is_empty() {
|
||||
return Some(Check::new(CheckKind::AssertTuple, location));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check NotInTest and NotIsTest compliance.
|
||||
pub fn check_not_tests(
|
||||
op: &Unaryop,
|
||||
operand: &Expr,
|
||||
check_not_in: bool,
|
||||
check_not_is: bool,
|
||||
) -> Vec<Check> {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
if matches!(op, Unaryop::Not) {
|
||||
if let ExprKind::Compare { ops, .. } = &operand.node {
|
||||
match ops[..] {
|
||||
[Cmpop::In] => {
|
||||
if check_not_in {
|
||||
checks.push(Check::new(CheckKind::NotInTest, operand.location));
|
||||
}
|
||||
}
|
||||
[Cmpop::Is] => {
|
||||
if check_not_is {
|
||||
checks.push(Check::new(CheckKind::NotIsTest, operand.location));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checks
|
||||
}
|
||||
|
||||
/// Check UnusedVariable compliance.
|
||||
pub fn check_unused_variables(scope: &Scope) -> Vec<Check> {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
for (name, binding) in scope.values.iter() {
|
||||
// TODO(charlie): Ignore if using `locals`.
|
||||
if binding.used.is_none()
|
||||
&& name != "_"
|
||||
&& name != "__tracebackhide__"
|
||||
&& name != "__traceback_info__"
|
||||
&& name != "__traceback_supplement__"
|
||||
&& matches!(binding.kind, BindingKind::Assignment)
|
||||
{
|
||||
checks.push(Check::new(
|
||||
CheckKind::UnusedVariable(name.to_string()),
|
||||
binding.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
checks
|
||||
}
|
||||
|
||||
/// Check DoNotAssignLambda compliance.
|
||||
pub fn check_do_not_assign_lambda(value: &Expr, location: Location) -> Option<Check> {
|
||||
if let ExprKind::Lambda { .. } = &value.node {
|
||||
Some(Check::new(CheckKind::DoNotAssignLambda, location))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
if id == "object" {
|
||||
match scope.values.get(id) {
|
||||
None
|
||||
| Some(Binding {
|
||||
kind: BindingKind::Builtin,
|
||||
..
|
||||
}) => {
|
||||
let mut check = Check::new(
|
||||
CheckKind::UselessObjectInheritance(name.to_string()),
|
||||
expr.location,
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate)
|
||||
|| matches!(autofix, 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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check DefaultExceptNotLast compliance.
|
||||
pub fn check_default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Check> {
|
||||
for (idx, handler) in handlers.iter().enumerate() {
|
||||
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
|
||||
if type_.is_none() && idx < handlers.len() - 1 {
|
||||
return Some(Check::new(
|
||||
CheckKind::DefaultExceptNotLast,
|
||||
handler.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check RaiseNotImplemented compliance.
|
||||
pub fn check_raise_not_implemented(expr: &Expr) -> Option<Check> {
|
||||
match &expr.node {
|
||||
ExprKind::Call { func, .. } => {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "NotImplemented" {
|
||||
return Some(Check::new(CheckKind::RaiseNotImplemented, expr.location));
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprKind::Name { id, .. } => {
|
||||
if id == "NotImplemented" {
|
||||
return Some(Check::new(CheckKind::RaiseNotImplemented, expr.location));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check DuplicateArgumentName compliance.
|
||||
pub fn check_duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
// Collect all the arguments into a single vector.
|
||||
let mut all_arguments: Vec<&Arg> = arguments
|
||||
.args
|
||||
.iter()
|
||||
.chain(arguments.posonlyargs.iter())
|
||||
.chain(arguments.kwonlyargs.iter())
|
||||
.collect();
|
||||
if let Some(arg) = &arguments.vararg {
|
||||
all_arguments.push(arg);
|
||||
}
|
||||
if let Some(arg) = &arguments.kwarg {
|
||||
all_arguments.push(arg);
|
||||
}
|
||||
|
||||
// Search for duplicates.
|
||||
let mut idents: BTreeSet<&str> = BTreeSet::new();
|
||||
for arg in all_arguments {
|
||||
let ident = &arg.node.arg;
|
||||
if idents.contains(ident.as_str()) {
|
||||
checks.push(Check::new(CheckKind::DuplicateArgumentName, arg.location));
|
||||
}
|
||||
idents.insert(ident);
|
||||
}
|
||||
|
||||
checks
|
||||
}
|
||||
|
||||
/// Check AssertEquals compliance.
|
||||
pub fn check_assert_equals(expr: &Expr, autofix: &fixer::Mode) -> 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, expr.location);
|
||||
if matches!(autofix, fixer::Mode::Generate)
|
||||
|| matches!(autofix, fixer::Mode::Apply)
|
||||
{
|
||||
check.amend(Fix {
|
||||
content: "assertEqual".to_string(),
|
||||
start: Location::new(expr.location.row(), expr.location.column() + 1),
|
||||
end: Location::new(
|
||||
expr.location.row(),
|
||||
expr.location.column() + 1 + "assertEquals".len(),
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
}
|
||||
return Some(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DictionaryKey<'a> {
|
||||
Constant(&'a Constant),
|
||||
Variable(&'a String),
|
||||
}
|
||||
|
||||
fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
|
||||
match &expr.node {
|
||||
ExprKind::Constant { value, .. } => Some(DictionaryKey::Constant(value)),
|
||||
ExprKind::Name { id, .. } => Some(DictionaryKey::Variable(id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check MultiValueRepeatedKeyLiteral and MultiValueRepeatedKeyVariable compliance.
|
||||
pub fn check_repeated_keys(
|
||||
keys: &Vec<Expr>,
|
||||
check_repeated_literals: bool,
|
||||
check_repeated_variables: bool,
|
||||
) -> Vec<Check> {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
let num_keys = keys.len();
|
||||
for i in 0..num_keys {
|
||||
let k1 = &keys[i];
|
||||
let v1 = convert_to_value(k1);
|
||||
for k2 in keys.iter().take(num_keys).skip(i + 1) {
|
||||
let v2 = convert_to_value(k2);
|
||||
match (&v1, &v2) {
|
||||
(Some(DictionaryKey::Constant(v1)), Some(DictionaryKey::Constant(v2))) => {
|
||||
if check_repeated_literals && v1 == v2 {
|
||||
checks.push(Check::new(
|
||||
CheckKind::MultiValueRepeatedKeyLiteral,
|
||||
k2.location,
|
||||
))
|
||||
}
|
||||
}
|
||||
(Some(DictionaryKey::Variable(v1)), Some(DictionaryKey::Variable(v2))) => {
|
||||
if check_repeated_variables && v1 == v2 {
|
||||
checks.push(Check::new(
|
||||
CheckKind::MultiValueRepeatedKeyVariable(v2.to_string()),
|
||||
k2.location,
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checks
|
||||
}
|
||||
|
||||
/// Check TrueFalseComparison and NoneComparison compliance.
|
||||
pub fn check_literal_comparisons(
|
||||
left: &Expr,
|
||||
ops: &Vec<Cmpop>,
|
||||
comparators: &Vec<Expr>,
|
||||
check_none_comparisons: bool,
|
||||
check_true_false_comparisons: bool,
|
||||
) -> Vec<Check> {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
let op = ops.first().unwrap();
|
||||
let comparator = left;
|
||||
|
||||
// Check `left`.
|
||||
if check_none_comparisons
|
||||
&& matches!(
|
||||
comparator.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::None,
|
||||
kind: None
|
||||
}
|
||||
)
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if check_true_false_comparisons {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Bool(value),
|
||||
kind: None,
|
||||
} = comparator.node
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check each comparator in order.
|
||||
for (op, comparator) in izip!(ops, comparators) {
|
||||
if check_none_comparisons
|
||||
&& matches!(
|
||||
comparator.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::None,
|
||||
kind: None
|
||||
}
|
||||
)
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if check_true_false_comparisons {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Bool(value),
|
||||
kind: None,
|
||||
} = comparator.node
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checks
|
||||
}
|
||||
|
||||
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
|
||||
pub fn check_starred_expressions(
|
||||
elts: &[Expr],
|
||||
location: Location,
|
||||
check_too_many_expressions: bool,
|
||||
check_two_starred_expressions: bool,
|
||||
) -> Option<Check> {
|
||||
let mut has_starred: bool = false;
|
||||
let mut starred_index: Option<usize> = None;
|
||||
for (index, elt) in elts.iter().enumerate() {
|
||||
if matches!(elt.node, ExprKind::Starred { .. }) {
|
||||
if has_starred && check_two_starred_expressions {
|
||||
return Some(Check::new(CheckKind::TwoStarredExpressions, location));
|
||||
}
|
||||
has_starred = true;
|
||||
starred_index = Some(index);
|
||||
}
|
||||
}
|
||||
|
||||
if check_too_many_expressions {
|
||||
if let Some(starred_index) = starred_index {
|
||||
if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 {
|
||||
return Some(Check::new(
|
||||
CheckKind::TooManyExpressionsInStarredAssignment,
|
||||
location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -1,58 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
|
||||
|
||||
fn id() -> usize {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ScopeKind {
|
||||
Class,
|
||||
Function,
|
||||
Generator,
|
||||
Module,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Scope {
|
||||
pub id: usize,
|
||||
pub kind: ScopeKind,
|
||||
pub values: BTreeMap<String, Binding>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub fn new(kind: ScopeKind) -> Self {
|
||||
Scope {
|
||||
id: id(),
|
||||
kind,
|
||||
values: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BindingKind {
|
||||
Argument,
|
||||
Assignment,
|
||||
Builtin,
|
||||
ClassDefinition,
|
||||
Definition,
|
||||
Export(Vec<String>),
|
||||
FutureImportation,
|
||||
Importation(String),
|
||||
StarImportation,
|
||||
SubmoduleImportation(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Binding {
|
||||
pub kind: BindingKind,
|
||||
pub location: Location,
|
||||
pub used: Option<usize>,
|
||||
}
|
||||
use crate::ast::types::{BindingKind, Scope};
|
||||
|
||||
/// Extract the names bound to a given __all__ assignment.
|
||||
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
||||
@@ -118,6 +66,40 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
||||
names
|
||||
}
|
||||
|
||||
/// Check if a node is parent of a conditional branch.
|
||||
pub fn on_conditional_branch(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
|
||||
for index in parent_stack.iter().rev() {
|
||||
let parent = parents[*index];
|
||||
if matches!(parent.node, StmtKind::If { .. })
|
||||
|| matches!(parent.node, StmtKind::While { .. })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let StmtKind::Expr { value } = &parent.node {
|
||||
if matches!(value.node, ExprKind::IfExp { .. }) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a node is in a nested block.
|
||||
pub fn in_nested_block(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
|
||||
for index in parent_stack.iter().rev() {
|
||||
let parent = parents[*index];
|
||||
if matches!(parent.node, StmtKind::Try { .. })
|
||||
|| matches!(parent.node, StmtKind::If { .. })
|
||||
|| matches!(parent.node, StmtKind::With { .. })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Struct used to efficiently slice source code at (row, column) Locations.
|
||||
pub struct SourceCodeLocator<'a> {
|
||||
content: &'a str,
|
||||
55
src/ast/types.rs
Normal file
55
src/ast/types.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
fn id() -> usize {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ScopeKind {
|
||||
Class,
|
||||
Function,
|
||||
Generator,
|
||||
Module,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Scope {
|
||||
pub id: usize,
|
||||
pub kind: ScopeKind,
|
||||
pub values: BTreeMap<String, Binding>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub fn new(kind: ScopeKind) -> Self {
|
||||
Scope {
|
||||
id: id(),
|
||||
kind,
|
||||
values: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BindingKind {
|
||||
Argument,
|
||||
Assignment,
|
||||
Builtin,
|
||||
ClassDefinition,
|
||||
Definition,
|
||||
Export(Vec<String>),
|
||||
FutureImportation,
|
||||
Importation(String),
|
||||
StarImportation,
|
||||
SubmoduleImportation(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Binding {
|
||||
pub kind: BindingKind,
|
||||
pub location: Location,
|
||||
pub used: Option<usize>,
|
||||
}
|
||||
218
src/autofix.rs
218
src/autofix.rs
@@ -1,216 +1,2 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{Check, Fix};
|
||||
|
||||
#[derive(Hash)]
|
||||
pub enum Mode {
|
||||
Generate,
|
||||
Apply,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => Mode::Apply,
|
||||
false => Mode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file(checks: &mut [Check], contents: &str, path: &Path) -> Result<()> {
|
||||
if checks.iter().all(|check| check.fix.is_none()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = apply_fixes(
|
||||
checks.iter_mut().filter_map(|check| check.fix.as_mut()),
|
||||
contents,
|
||||
);
|
||||
|
||||
fs::write(path, output).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Apply a series of fixes.
|
||||
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
|
||||
let mut output = "".to_string();
|
||||
let mut last_pos: Location = Location::new(0, 0);
|
||||
|
||||
for fix in fixes {
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos > fix.start {
|
||||
continue;
|
||||
}
|
||||
|
||||
if fix.start.row() > last_pos.row() {
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..fix.start.row() - 1] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&lines[fix.start.row() - 1][..fix.start.column() - 1]);
|
||||
output.push_str(&fix.content);
|
||||
} else {
|
||||
output.push_str(
|
||||
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.start.column() - 1],
|
||||
);
|
||||
output.push_str(&fix.content);
|
||||
}
|
||||
|
||||
last_pos = fix.end;
|
||||
fix.applied = true;
|
||||
}
|
||||
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::apply_fixes;
|
||||
use crate::checks::Fix;
|
||||
|
||||
#[test]
|
||||
fn empty_file() -> Result<()> {
|
||||
let mut fixes = vec![];
|
||||
let actual = apply_fixes(fixes.iter_mut(), "");
|
||||
let expected = "";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_replacement() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
start: Location::new(1, 9),
|
||||
end: Location::new(1, 15),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A(Bar):
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_removal() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 16),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_double_removal() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 17),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 17),
|
||||
end: Location::new(1, 24),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object, object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_overlapping_fixes() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 16),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
start: Location::new(1, 10),
|
||||
end: Location::new(1, 12),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
pub mod fixer;
|
||||
pub mod fixes;
|
||||
|
||||
216
src/autofix/fixer.rs
Normal file
216
src/autofix/fixer.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{Check, Fix};
|
||||
|
||||
#[derive(Hash)]
|
||||
pub enum Mode {
|
||||
Generate,
|
||||
Apply,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => Mode::Apply,
|
||||
false => Mode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file(checks: &mut [Check], contents: &str, path: &Path) -> Result<()> {
|
||||
if checks.iter().all(|check| check.fix.is_none()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = apply_fixes(
|
||||
checks.iter_mut().filter_map(|check| check.fix.as_mut()),
|
||||
contents,
|
||||
);
|
||||
|
||||
fs::write(path, output).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Apply a series of fixes.
|
||||
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
|
||||
let mut output = "".to_string();
|
||||
let mut last_pos: Location = Location::new(0, 0);
|
||||
|
||||
for fix in fixes {
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos > fix.start {
|
||||
continue;
|
||||
}
|
||||
|
||||
if fix.start.row() > last_pos.row() {
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..fix.start.row() - 1] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&lines[fix.start.row() - 1][..fix.start.column() - 1]);
|
||||
output.push_str(&fix.content);
|
||||
} else {
|
||||
output.push_str(
|
||||
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.start.column() - 1],
|
||||
);
|
||||
output.push_str(&fix.content);
|
||||
}
|
||||
|
||||
last_pos = fix.end;
|
||||
fix.applied = true;
|
||||
}
|
||||
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::fixer::apply_fixes;
|
||||
use crate::checks::Fix;
|
||||
|
||||
#[test]
|
||||
fn empty_file() -> Result<()> {
|
||||
let mut fixes = vec![];
|
||||
let actual = apply_fixes(fixes.iter_mut(), "");
|
||||
let expected = "";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_replacement() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
start: Location::new(1, 9),
|
||||
end: Location::new(1, 15),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A(Bar):
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_removal() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 16),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_double_removal() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 17),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 17),
|
||||
end: Location::new(1, 24),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object, object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_overlapping_fixes() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 16),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
start: Location::new(1, 10),
|
||||
end: Location::new(1, 12),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Location};
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::token::Tok;
|
||||
|
||||
use crate::ast_ops::SourceCodeLocator;
|
||||
use crate::ast::operations::SourceCodeLocator;
|
||||
use crate::checks::Fix;
|
||||
|
||||
/// Convert a location within a file (relative to `base`) to an absolute position.
|
||||
@@ -8,7 +8,7 @@ use filetime::FileTime;
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::autofix;
|
||||
use crate::autofix::fixer;
|
||||
use crate::message::Message;
|
||||
use crate::settings::Settings;
|
||||
|
||||
@@ -71,7 +71,7 @@ fn cache_dir() -> &'static str {
|
||||
"./.ruff_cache"
|
||||
}
|
||||
|
||||
fn cache_key(path: &Path, settings: &Settings, autofix: &autofix::Mode) -> String {
|
||||
fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
settings.hash(&mut hasher);
|
||||
autofix.hash(&mut hasher);
|
||||
@@ -87,7 +87,7 @@ pub fn get(
|
||||
path: &Path,
|
||||
metadata: &Metadata,
|
||||
settings: &Settings,
|
||||
autofix: &autofix::Mode,
|
||||
autofix: &fixer::Mode,
|
||||
mode: &Mode,
|
||||
) -> Option<Vec<Message>> {
|
||||
if !mode.allow_read() {
|
||||
@@ -116,7 +116,7 @@ pub fn set(
|
||||
path: &Path,
|
||||
metadata: &Metadata,
|
||||
settings: &Settings,
|
||||
autofix: &autofix::Mode,
|
||||
autofix: &fixer::Mode,
|
||||
messages: &[Message],
|
||||
mode: &Mode,
|
||||
) {
|
||||
|
||||
706
src/check_ast.rs
706
src/check_ast.rs
@@ -1,22 +1,21 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
use itertools::izip;
|
||||
use rustpython_parser::ast::{
|
||||
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
|
||||
KeywordData, Location, Stmt, StmtKind, Suite, Unaryop,
|
||||
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
|
||||
KeywordData, Location, Stmt, StmtKind, Suite,
|
||||
};
|
||||
use rustpython_parser::parser;
|
||||
|
||||
use crate::ast_ops::{
|
||||
extract_all_names, Binding, BindingKind, Scope, ScopeKind, SourceCodeLocator,
|
||||
};
|
||||
use crate::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use crate::checks::{Check, CheckCode, CheckKind, Fix, RejectedCmpop};
|
||||
use crate::relocator::relocate_expr;
|
||||
use crate::ast::operations::{extract_all_names, SourceCodeLocator};
|
||||
use crate::ast::relocate::relocate_expr;
|
||||
use crate::ast::types::{Binding, BindingKind, Scope, ScopeKind};
|
||||
use crate::ast::visitor::{walk_excepthandler, Visitor};
|
||||
use crate::ast::{checks, operations, visitor};
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use crate::python::typing;
|
||||
use crate::settings::Settings;
|
||||
use crate::visitor::{walk_excepthandler, Visitor};
|
||||
use crate::{autofix, fixer, visitor};
|
||||
|
||||
pub const GLOBAL_SCOPE_INDEX: usize = 0;
|
||||
|
||||
@@ -24,7 +23,7 @@ struct Checker<'a> {
|
||||
// Input data.
|
||||
locator: SourceCodeLocator<'a>,
|
||||
settings: &'a Settings,
|
||||
autofix: &'a autofix::Mode,
|
||||
autofix: &'a fixer::Mode,
|
||||
path: &'a str,
|
||||
// Computed checks.
|
||||
checks: Vec<Check>,
|
||||
@@ -38,6 +37,7 @@ struct Checker<'a> {
|
||||
deferred_annotations: Vec<(Location, &'a str)>,
|
||||
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>)>,
|
||||
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
|
||||
deferred_assignments: Vec<usize>,
|
||||
// Derivative state.
|
||||
in_f_string: bool,
|
||||
in_annotation: bool,
|
||||
@@ -49,7 +49,7 @@ struct Checker<'a> {
|
||||
impl<'a> Checker<'a> {
|
||||
pub fn new(
|
||||
settings: &'a Settings,
|
||||
autofix: &'a autofix::Mode,
|
||||
autofix: &'a fixer::Mode,
|
||||
path: &'a str,
|
||||
content: &'a str,
|
||||
) -> Checker<'a> {
|
||||
@@ -67,6 +67,7 @@ impl<'a> Checker<'a> {
|
||||
deferred_annotations: vec![],
|
||||
deferred_functions: vec![],
|
||||
deferred_lambdas: vec![],
|
||||
deferred_assignments: vec![],
|
||||
in_f_string: false,
|
||||
in_annotation: false,
|
||||
in_literal: false,
|
||||
@@ -76,20 +77,6 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DictionaryKey<'a> {
|
||||
Constant(&'a Constant),
|
||||
Variable(&'a String),
|
||||
}
|
||||
|
||||
fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
|
||||
match &expr.node {
|
||||
ExprKind::Constant { value, .. } => Some(DictionaryKey::Constant(value)),
|
||||
ExprKind::Name { id, .. } => Some(DictionaryKey::Variable(id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
|
||||
match &expr.node {
|
||||
ExprKind::Attribute { attr, .. } => target == attr,
|
||||
@@ -98,6 +85,14 @@ fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_annotated_subscript(expr: &Expr) -> bool {
|
||||
match &expr.node {
|
||||
ExprKind::Attribute { attr, .. } => typing::is_annotated_subscript(attr),
|
||||
ExprKind::Name { id, .. } => typing::is_annotated_subscript(id),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for Checker<'a>
|
||||
where
|
||||
'b: 'a,
|
||||
@@ -105,6 +100,41 @@ where
|
||||
fn visit_stmt(&mut self, stmt: &'b Stmt) {
|
||||
self.push_parent(stmt);
|
||||
|
||||
// Track whether we've seen docstrings, non-imports, etc.
|
||||
match &stmt.node {
|
||||
StmtKind::Import { .. } => {}
|
||||
StmtKind::ImportFrom { .. } => {}
|
||||
StmtKind::Expr { value } => {
|
||||
if !self.seen_docstring
|
||||
&& stmt.location.column() == 1
|
||||
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
|
||||
{
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(_),
|
||||
..
|
||||
} = &value.node
|
||||
{
|
||||
self.seen_docstring = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.seen_non_import
|
||||
&& stmt.location.column() == 1
|
||||
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
|
||||
{
|
||||
self.seen_non_import = true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if !self.seen_non_import
|
||||
&& stmt.location.column() == 1
|
||||
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
|
||||
{
|
||||
self.seen_non_import = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-visit.
|
||||
match &stmt.node {
|
||||
StmtKind::Global { names } | StmtKind::Nonlocal { names } => {
|
||||
@@ -133,20 +163,53 @@ where
|
||||
name,
|
||||
decorator_list,
|
||||
returns,
|
||||
args,
|
||||
..
|
||||
}
|
||||
| StmtKind::AsyncFunctionDef {
|
||||
name,
|
||||
decorator_list,
|
||||
returns,
|
||||
args,
|
||||
..
|
||||
} => {
|
||||
for expr in decorator_list {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
for arg in &args.posonlyargs {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
self.visit_annotation(expr);
|
||||
}
|
||||
}
|
||||
for arg in &args.args {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
self.visit_annotation(expr);
|
||||
}
|
||||
}
|
||||
if let Some(arg) = &args.vararg {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
self.visit_annotation(expr);
|
||||
}
|
||||
}
|
||||
for arg in &args.kwonlyargs {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
self.visit_annotation(expr);
|
||||
}
|
||||
}
|
||||
if let Some(arg) = &args.kwarg {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
self.visit_annotation(expr);
|
||||
}
|
||||
}
|
||||
for expr in returns {
|
||||
self.visit_annotation(expr);
|
||||
}
|
||||
for expr in &args.kw_defaults {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
for expr in &args.defaults {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.add_binding(
|
||||
name.to_string(),
|
||||
Binding {
|
||||
@@ -183,41 +246,18 @@ where
|
||||
..
|
||||
} => {
|
||||
if self.settings.select.contains(&CheckCode::R001) {
|
||||
for expr in bases {
|
||||
if let ExprKind::Name { id, .. } = &expr.node {
|
||||
if id == "object" {
|
||||
let scope = &self.scopes
|
||||
[*(self.scope_stack.last().expect("No current scope found."))];
|
||||
match scope.values.get(id) {
|
||||
None
|
||||
| Some(Binding {
|
||||
kind: BindingKind::Builtin,
|
||||
..
|
||||
}) => {
|
||||
let mut check = Check::new(
|
||||
CheckKind::UselessObjectInheritance(name.to_string()),
|
||||
expr.location,
|
||||
);
|
||||
if matches!(self.autofix, autofix::Mode::Generate)
|
||||
|| matches!(self.autofix, autofix::Mode::Apply)
|
||||
{
|
||||
if let Some(fix) = fixer::remove_class_def_base(
|
||||
&mut self.locator,
|
||||
&stmt.location,
|
||||
expr.location,
|
||||
bases,
|
||||
keywords,
|
||||
) {
|
||||
check.amend(fix);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
self.checks.push(check);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,11 +367,7 @@ where
|
||||
},
|
||||
);
|
||||
|
||||
if self
|
||||
.settings
|
||||
.select
|
||||
.contains(CheckKind::ImportStarUsage.code())
|
||||
{
|
||||
if self.settings.select.contains(&CheckCode::F403) {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::ImportStarUsage, stmt.location));
|
||||
}
|
||||
@@ -348,123 +384,58 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::If { test, .. } => {
|
||||
if self.settings.select.contains(CheckKind::IfTuple.code()) {
|
||||
if let ExprKind::Tuple { elts, .. } = &test.node {
|
||||
if !elts.is_empty() {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::IfTuple, stmt.location));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Raise { exc, .. } => {
|
||||
if self
|
||||
.settings
|
||||
.select
|
||||
.contains(CheckKind::RaiseNotImplemented.code())
|
||||
{
|
||||
if self.settings.select.contains(&CheckCode::F901) {
|
||||
if let Some(expr) = exc {
|
||||
match &expr.node {
|
||||
ExprKind::Call { func, .. } => {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "NotImplemented" {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::RaiseNotImplemented,
|
||||
stmt.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprKind::Name { id, .. } => {
|
||||
if id == "NotImplemented" {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::RaiseNotImplemented,
|
||||
stmt.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
if let Some(check) = checks::check_raise_not_implemented(expr) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::AugAssign { target, .. } => {
|
||||
self.seen_non_import = true;
|
||||
self.handle_node_load(target);
|
||||
}
|
||||
StmtKind::If { test, .. } => {
|
||||
if self.settings.select.contains(&CheckCode::F634) {
|
||||
if let Some(check) = checks::check_if_tuple(test, stmt.location) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Assert { test, .. } => {
|
||||
self.seen_non_import = true;
|
||||
if self.settings.select.contains(CheckKind::AssertTuple.code()) {
|
||||
if let ExprKind::Tuple { elts, .. } = &test.node {
|
||||
if !elts.is_empty() {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::AssertTuple, stmt.location));
|
||||
}
|
||||
if let Some(check) = checks::check_assert_tuple(test, stmt.location) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Try { handlers, .. } => {
|
||||
if self
|
||||
.settings
|
||||
.select
|
||||
.contains(CheckKind::DefaultExceptNotLast.code())
|
||||
{
|
||||
for (idx, handler) in handlers.iter().enumerate() {
|
||||
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
|
||||
if type_.is_none() && idx < handlers.len() - 1 {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::DefaultExceptNotLast,
|
||||
handler.location,
|
||||
));
|
||||
}
|
||||
if self.settings.select.contains(&CheckCode::F707) {
|
||||
if let Some(check) = checks::check_default_except_not_last(handlers) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Expr { value } => {
|
||||
if !self.seen_docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(_),
|
||||
..
|
||||
} = &value.node
|
||||
{
|
||||
self.seen_docstring = true;
|
||||
}
|
||||
} else {
|
||||
self.seen_non_import = true;
|
||||
}
|
||||
}
|
||||
StmtKind::Assign { value, .. } => {
|
||||
self.seen_non_import = true;
|
||||
if self
|
||||
.settings
|
||||
.select
|
||||
.contains(CheckKind::DoNotAssignLambda.code())
|
||||
{
|
||||
if let ExprKind::Lambda { .. } = &value.node {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::DoNotAssignLambda, stmt.location));
|
||||
if self.settings.select.contains(&CheckCode::E731) {
|
||||
if let Some(check) = checks::check_do_not_assign_lambda(value, stmt.location) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::AnnAssign { value, .. } => {
|
||||
self.seen_non_import = true;
|
||||
if self
|
||||
.settings
|
||||
.select
|
||||
.contains(CheckKind::DoNotAssignLambda.code())
|
||||
{
|
||||
if let Some(v) = value {
|
||||
if let ExprKind::Lambda { .. } = v.node {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::DoNotAssignLambda, stmt.location));
|
||||
if self.settings.select.contains(&CheckCode::E731) {
|
||||
if let Some(value) = value {
|
||||
if let Some(check) =
|
||||
checks::check_do_not_assign_lambda(value, stmt.location)
|
||||
{
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Delete { .. } => {
|
||||
self.seen_non_import = true;
|
||||
}
|
||||
StmtKind::Delete { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -511,6 +482,7 @@ where
|
||||
fn visit_expr(&mut self, expr: &'b Expr) {
|
||||
let prev_in_f_string = self.in_f_string;
|
||||
let prev_in_literal = self.in_literal;
|
||||
let prev_in_annotation = self.in_annotation;
|
||||
|
||||
// Pre-visit.
|
||||
match &expr.node {
|
||||
@@ -519,6 +491,22 @@ where
|
||||
self.in_literal = true;
|
||||
}
|
||||
}
|
||||
ExprKind::Tuple { elts, ctx } => {
|
||||
if matches!(ctx, ExprContext::Store) {
|
||||
let check_too_many_expressions =
|
||||
self.settings.select.contains(&CheckCode::F621);
|
||||
let check_two_starred_expressions =
|
||||
self.settings.select.contains(&CheckCode::F622);
|
||||
if let Some(check) = checks::check_starred_expressions(
|
||||
elts,
|
||||
expr.location,
|
||||
check_too_many_expressions,
|
||||
check_two_starred_expressions,
|
||||
) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprKind::Name { ctx, .. } => match ctx {
|
||||
ExprContext::Load => self.handle_node_load(expr),
|
||||
ExprContext::Store => {
|
||||
@@ -530,81 +518,23 @@ where
|
||||
},
|
||||
ExprKind::Call { func, .. } => {
|
||||
if self.settings.select.contains(&CheckCode::R002) {
|
||||
if let ExprKind::Attribute { value, attr, .. } = &func.node {
|
||||
if attr == "assertEquals" {
|
||||
if let ExprKind::Name { id, .. } = &value.node {
|
||||
if id == "self" {
|
||||
let mut check =
|
||||
Check::new(CheckKind::NoAssertEquals, expr.location);
|
||||
if matches!(self.autofix, autofix::Mode::Generate)
|
||||
|| matches!(self.autofix, autofix::Mode::Apply)
|
||||
{
|
||||
check.amend(Fix {
|
||||
content: "assertEqual".to_string(),
|
||||
start: Location::new(
|
||||
func.location.row(),
|
||||
func.location.column() + 1,
|
||||
),
|
||||
end: Location::new(
|
||||
func.location.row(),
|
||||
func.location.column() + 1 + "assertEquals".len(),
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
}
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(check) = checks::check_assert_equals(func, self.autofix) {
|
||||
self.checks.push(check)
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprKind::Dict { keys, .. } => {
|
||||
if self.settings.select.contains(&CheckCode::F601)
|
||||
|| self.settings.select.contains(&CheckCode::F602)
|
||||
{
|
||||
let num_keys = keys.len();
|
||||
for i in 0..num_keys {
|
||||
let k1 = &keys[i];
|
||||
let v1 = convert_to_value(k1);
|
||||
for k2 in keys.iter().take(num_keys).skip(i + 1) {
|
||||
let v2 = convert_to_value(k2);
|
||||
match (&v1, &v2) {
|
||||
(
|
||||
Some(DictionaryKey::Constant(v1)),
|
||||
Some(DictionaryKey::Constant(v2)),
|
||||
) => {
|
||||
if self.settings.select.contains(&CheckCode::F601) && v1 == v2 {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::MultiValueRepeatedKeyLiteral,
|
||||
k2.location,
|
||||
))
|
||||
}
|
||||
}
|
||||
(
|
||||
Some(DictionaryKey::Variable(v1)),
|
||||
Some(DictionaryKey::Variable(v2)),
|
||||
) => {
|
||||
if self.settings.select.contains(&CheckCode::F602) && v1 == v2 {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::MultiValueRepeatedKeyVariable(
|
||||
v2.to_string(),
|
||||
),
|
||||
k2.location,
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let check_repeated_literals = self.settings.select.contains(&CheckCode::F601);
|
||||
let check_repeated_variables = self.settings.select.contains(&CheckCode::F602);
|
||||
if check_repeated_literals || check_repeated_variables {
|
||||
self.checks.extend(checks::check_repeated_keys(
|
||||
keys,
|
||||
check_repeated_literals,
|
||||
check_repeated_variables,
|
||||
));
|
||||
}
|
||||
}
|
||||
ExprKind::GeneratorExp { .. }
|
||||
| ExprKind::ListComp { .. }
|
||||
| ExprKind::DictComp { .. }
|
||||
| ExprKind::SetComp { .. } => self.push_scope(Scope::new(ScopeKind::Generator)),
|
||||
ExprKind::Yield { .. } | ExprKind::YieldFrom { .. } => {
|
||||
ExprKind::Yield { .. } | ExprKind::YieldFrom { .. } | ExprKind::Await { .. } => {
|
||||
let scope =
|
||||
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
|
||||
if self
|
||||
@@ -636,24 +566,15 @@ where
|
||||
self.in_f_string = true;
|
||||
}
|
||||
ExprKind::UnaryOp { op, operand } => {
|
||||
if matches!(op, Unaryop::Not) {
|
||||
if let ExprKind::Compare { ops, .. } = &operand.node {
|
||||
match ops[..] {
|
||||
[Cmpop::In] => {
|
||||
if self.settings.select.contains(CheckKind::NotInTest.code()) {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::NotInTest, operand.location));
|
||||
}
|
||||
}
|
||||
[Cmpop::Is] => {
|
||||
if self.settings.select.contains(CheckKind::NotIsTest.code()) {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::NotIsTest, operand.location));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let check_not_in = self.settings.select.contains(&CheckCode::E713);
|
||||
let check_not_is = self.settings.select.contains(&CheckCode::E714);
|
||||
if check_not_in || check_not_is {
|
||||
self.checks.extend(checks::check_not_tests(
|
||||
op,
|
||||
operand,
|
||||
check_not_in,
|
||||
check_not_is,
|
||||
));
|
||||
}
|
||||
}
|
||||
ExprKind::Compare {
|
||||
@@ -661,99 +582,16 @@ where
|
||||
ops,
|
||||
comparators,
|
||||
} => {
|
||||
let op = ops.first().unwrap();
|
||||
let comparator = left;
|
||||
|
||||
// Check `left`.
|
||||
if self.settings.select.contains(&CheckCode::E711)
|
||||
&& matches!(
|
||||
comparator.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::None,
|
||||
kind: None
|
||||
}
|
||||
)
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.select.contains(&CheckCode::E712) {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Bool(value),
|
||||
kind: None,
|
||||
} = comparator.node
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check each comparator in order.
|
||||
for (op, comparator) in izip!(ops, comparators) {
|
||||
if self.settings.select.contains(&CheckCode::E711)
|
||||
&& matches!(
|
||||
comparator.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::None,
|
||||
kind: None
|
||||
}
|
||||
)
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.select.contains(&CheckCode::E712) {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Bool(value),
|
||||
kind: None,
|
||||
} = comparator.node
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let check_none_comparisons = self.settings.select.contains(&CheckCode::E711);
|
||||
let check_true_false_comparisons = self.settings.select.contains(&CheckCode::E712);
|
||||
if check_none_comparisons || check_true_false_comparisons {
|
||||
self.checks.extend(checks::check_literal_comparisons(
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
check_none_comparisons,
|
||||
check_true_false_comparisons,
|
||||
));
|
||||
}
|
||||
}
|
||||
ExprKind::Constant {
|
||||
@@ -762,6 +600,10 @@ where
|
||||
} if self.in_annotation && !self.in_literal => {
|
||||
self.deferred_annotations.push((expr.location, value));
|
||||
}
|
||||
ExprKind::GeneratorExp { .. }
|
||||
| ExprKind::ListComp { .. }
|
||||
| ExprKind::DictComp { .. }
|
||||
| ExprKind::SetComp { .. } => self.push_scope(Scope::new(ScopeKind::Generator)),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
@@ -779,7 +621,25 @@ where
|
||||
args,
|
||||
keywords,
|
||||
} => {
|
||||
if match_name_or_attr(func, "TypeVar") {
|
||||
if match_name_or_attr(func, "ForwardRef") {
|
||||
self.visit_expr(func);
|
||||
for expr in args {
|
||||
self.visit_annotation(expr);
|
||||
}
|
||||
} else if match_name_or_attr(func, "cast") {
|
||||
self.visit_expr(func);
|
||||
if !args.is_empty() {
|
||||
self.visit_annotation(&args[0]);
|
||||
}
|
||||
for expr in args.iter().skip(1) {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
} else if match_name_or_attr(func, "NewType") {
|
||||
self.visit_expr(func);
|
||||
for expr in args.iter().skip(1) {
|
||||
self.visit_annotation(expr);
|
||||
}
|
||||
} else if match_name_or_attr(func, "TypeVar") {
|
||||
self.visit_expr(func);
|
||||
for expr in args.iter().skip(1) {
|
||||
self.visit_annotation(expr);
|
||||
@@ -790,16 +650,72 @@ where
|
||||
if id == "bound" {
|
||||
self.visit_annotation(value);
|
||||
} else {
|
||||
self.in_annotation = false;
|
||||
self.visit_expr(value);
|
||||
self.in_annotation = prev_in_annotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if match_name_or_attr(func, "NamedTuple") {
|
||||
self.visit_expr(func);
|
||||
|
||||
// NamedTuple("a", [("a", int)])
|
||||
if args.len() > 1 {
|
||||
match &args[1].node {
|
||||
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
|
||||
for elt in elts {
|
||||
match &elt.node {
|
||||
ExprKind::List { elts, .. }
|
||||
| ExprKind::Tuple { elts, .. } => {
|
||||
if elts.len() == 2 {
|
||||
self.in_annotation = false;
|
||||
self.visit_expr(&elts[0]);
|
||||
self.in_annotation = prev_in_annotation;
|
||||
|
||||
self.visit_annotation(&elts[1]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// NamedTuple("a", a=int)
|
||||
for keyword in keywords {
|
||||
let KeywordData { value, .. } = &keyword.node;
|
||||
self.visit_annotation(value);
|
||||
}
|
||||
} else if match_name_or_attr(func, "TypedDict") {
|
||||
self.visit_expr(func);
|
||||
|
||||
// TypedDict("a", {"a": int})
|
||||
if args.len() > 1 {
|
||||
if let ExprKind::Dict { keys, values } = &args[1].node {
|
||||
for key in keys {
|
||||
self.in_annotation = false;
|
||||
self.visit_expr(key);
|
||||
self.in_annotation = prev_in_annotation;
|
||||
}
|
||||
for value in values {
|
||||
self.visit_annotation(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TypedDict("a", a=int)
|
||||
for keyword in keywords {
|
||||
let KeywordData { value, .. } = &keyword.node;
|
||||
self.visit_annotation(value);
|
||||
}
|
||||
} else {
|
||||
visitor::walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
ExprKind::Subscript { value, slice, ctx } => {
|
||||
if match_name_or_attr(value, "Type") {
|
||||
if is_annotated_subscript(value) {
|
||||
self.visit_expr(value);
|
||||
self.visit_annotation(slice);
|
||||
self.visit_expr_context(ctx);
|
||||
@@ -820,6 +736,8 @@ where
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
self.in_annotation = prev_in_annotation;
|
||||
self.in_literal = prev_in_literal;
|
||||
self.in_f_string = prev_in_f_string;
|
||||
}
|
||||
@@ -887,42 +805,31 @@ where
|
||||
}
|
||||
|
||||
fn visit_arguments(&mut self, arguments: &'b Arguments) {
|
||||
if self
|
||||
.settings
|
||||
.select
|
||||
.contains(CheckKind::DuplicateArgumentName.code())
|
||||
{
|
||||
// Collect all the arguments into a single vector.
|
||||
let mut all_arguments: Vec<&Arg> = arguments
|
||||
.args
|
||||
.iter()
|
||||
.chain(arguments.posonlyargs.iter())
|
||||
.chain(arguments.kwonlyargs.iter())
|
||||
.collect();
|
||||
if let Some(arg) = &arguments.vararg {
|
||||
all_arguments.push(arg);
|
||||
}
|
||||
if let Some(arg) = &arguments.kwarg {
|
||||
all_arguments.push(arg);
|
||||
}
|
||||
|
||||
// Search for duplicates.
|
||||
let mut idents: BTreeSet<&str> = BTreeSet::new();
|
||||
for arg in all_arguments {
|
||||
let ident = &arg.node.arg;
|
||||
if idents.contains(ident.as_str()) {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::DuplicateArgumentName, arg.location));
|
||||
break;
|
||||
}
|
||||
idents.insert(ident);
|
||||
}
|
||||
if self.settings.select.contains(&CheckCode::F831) {
|
||||
self.checks
|
||||
.extend(checks::check_duplicate_arguments(arguments));
|
||||
}
|
||||
|
||||
visitor::walk_arguments(self, arguments);
|
||||
// Bind, but intentionally avoid walking default expressions, as we handle them upstream.
|
||||
for arg in &arguments.posonlyargs {
|
||||
self.visit_arg(arg);
|
||||
}
|
||||
for arg in &arguments.args {
|
||||
self.visit_arg(arg);
|
||||
}
|
||||
if let Some(arg) = &arguments.vararg {
|
||||
self.visit_arg(arg);
|
||||
}
|
||||
for arg in &arguments.kwonlyargs {
|
||||
self.visit_arg(arg);
|
||||
}
|
||||
if let Some(arg) = &arguments.kwarg {
|
||||
self.visit_arg(arg);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_arg(&mut self, arg: &'b Arg) {
|
||||
// Bind, but intentionally avoid walking the annotation, as we handle it upstream.
|
||||
self.add_binding(
|
||||
arg.node.arg.to_string(),
|
||||
Binding {
|
||||
@@ -931,7 +838,6 @@ where
|
||||
location: arg.location,
|
||||
},
|
||||
);
|
||||
visitor::walk_arg(self, arg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1098,6 +1004,11 @@ impl<'a> Checker<'a> {
|
||||
|
||||
fn handle_node_delete(&mut self, expr: &Expr) {
|
||||
if let ExprKind::Name { id, .. } = &expr.node {
|
||||
// Check if we're on a conditional branch.
|
||||
if operations::on_conditional_branch(&self.parent_stack, &self.parents) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scope =
|
||||
&mut self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
|
||||
if scope.values.remove(id).is_none() && self.settings.select.contains(&CheckCode::F821)
|
||||
@@ -1145,23 +1056,8 @@ impl<'a> Checker<'a> {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let scope = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
|
||||
for (name, binding) in scope.values.iter() {
|
||||
// TODO(charlie): Ignore if using `locals`.
|
||||
if self.settings.select.contains(&CheckCode::F841)
|
||||
&& binding.used.is_none()
|
||||
&& name != "_"
|
||||
&& name != "__tracebackhide__"
|
||||
&& name != "__traceback_info__"
|
||||
&& name != "__traceback_supplement__"
|
||||
&& matches!(binding.kind, BindingKind::Assignment)
|
||||
{
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::UnusedVariable(name.to_string()),
|
||||
binding.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
self.deferred_assignments
|
||||
.push(*self.scope_stack.last().expect("No current scope found."));
|
||||
|
||||
self.pop_scope();
|
||||
}
|
||||
@@ -1180,28 +1076,23 @@ impl<'a> Checker<'a> {
|
||||
self.visit_expr(body);
|
||||
}
|
||||
|
||||
let scope = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
|
||||
for (name, binding) in scope.values.iter() {
|
||||
// TODO(charlie): Ignore if using `locals`.
|
||||
if self.settings.select.contains(&CheckCode::F841)
|
||||
&& binding.used.is_none()
|
||||
&& name != "_"
|
||||
&& name != "__tracebackhide__"
|
||||
&& name != "__traceback_info__"
|
||||
&& name != "__traceback_supplement__"
|
||||
&& matches!(binding.kind, BindingKind::Assignment)
|
||||
{
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::UnusedVariable(name.to_string()),
|
||||
binding.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
self.deferred_assignments
|
||||
.push(*self.scope_stack.last().expect("No current scope found."));
|
||||
|
||||
self.pop_scope();
|
||||
}
|
||||
}
|
||||
|
||||
fn check_deferred_assignments(&mut self) {
|
||||
while !self.deferred_assignments.is_empty() {
|
||||
let index = self.deferred_assignments.pop().unwrap();
|
||||
if self.settings.select.contains(&CheckCode::F841) {
|
||||
self.checks
|
||||
.extend(checks::check_unused_variables(&self.scopes[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_dead_scopes(&mut self) {
|
||||
if !self.settings.select.contains(&CheckCode::F822)
|
||||
&& !self.settings.select.contains(&CheckCode::F401)
|
||||
@@ -1264,7 +1155,7 @@ pub fn check_ast(
|
||||
python_ast: &Suite,
|
||||
content: &str,
|
||||
settings: &Settings,
|
||||
autofix: &autofix::Mode,
|
||||
autofix: &fixer::Mode,
|
||||
path: &str,
|
||||
) -> Vec<Check> {
|
||||
let mut checker = Checker::new(settings, autofix, path, content);
|
||||
@@ -1279,6 +1170,7 @@ pub fn check_ast(
|
||||
// Check any deferred statements.
|
||||
checker.check_deferred_functions();
|
||||
checker.check_deferred_lambdas();
|
||||
checker.check_deferred_assignments();
|
||||
let mut allocator = vec![];
|
||||
checker.check_deferred_annotations(path, &mut allocator);
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ pub enum CheckCode {
|
||||
F541,
|
||||
F601,
|
||||
F602,
|
||||
F621,
|
||||
F622,
|
||||
F631,
|
||||
F634,
|
||||
F704,
|
||||
@@ -54,6 +56,8 @@ impl FromStr for CheckCode {
|
||||
"F541" => Ok(CheckCode::F541),
|
||||
"F601" => Ok(CheckCode::F601),
|
||||
"F602" => Ok(CheckCode::F602),
|
||||
"F621" => Ok(CheckCode::F621),
|
||||
"F622" => Ok(CheckCode::F622),
|
||||
"F631" => Ok(CheckCode::F631),
|
||||
"F634" => Ok(CheckCode::F634),
|
||||
"F704" => Ok(CheckCode::F704),
|
||||
@@ -88,6 +92,8 @@ impl CheckCode {
|
||||
CheckCode::F541 => "F541",
|
||||
CheckCode::F601 => "F601",
|
||||
CheckCode::F602 => "F602",
|
||||
CheckCode::F621 => "F621",
|
||||
CheckCode::F622 => "F622",
|
||||
CheckCode::F631 => "F631",
|
||||
CheckCode::F634 => "F634",
|
||||
CheckCode::F704 => "F704",
|
||||
@@ -120,6 +126,8 @@ impl CheckCode {
|
||||
CheckCode::F541 => &LintSource::AST,
|
||||
CheckCode::F601 => &LintSource::AST,
|
||||
CheckCode::F602 => &LintSource::AST,
|
||||
CheckCode::F621 => &LintSource::AST,
|
||||
CheckCode::F622 => &LintSource::AST,
|
||||
CheckCode::F631 => &LintSource::AST,
|
||||
CheckCode::F634 => &LintSource::AST,
|
||||
CheckCode::F704 => &LintSource::AST,
|
||||
@@ -170,7 +178,9 @@ pub enum CheckKind {
|
||||
NotIsTest,
|
||||
RaiseNotImplemented,
|
||||
ReturnOutsideFunction,
|
||||
TooManyExpressionsInStarredAssignment,
|
||||
TrueFalseComparison(bool, RejectedCmpop),
|
||||
TwoStarredExpressions,
|
||||
UndefinedExport(String),
|
||||
UndefinedLocal(String),
|
||||
UndefinedName(String),
|
||||
@@ -202,7 +212,11 @@ impl CheckKind {
|
||||
CheckKind::NotIsTest => "NotIsTest",
|
||||
CheckKind::RaiseNotImplemented => "RaiseNotImplemented",
|
||||
CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction",
|
||||
CheckKind::TooManyExpressionsInStarredAssignment => {
|
||||
"TooManyExpressionsInStarredAssignment"
|
||||
}
|
||||
CheckKind::TrueFalseComparison(_, _) => "TrueFalseComparison",
|
||||
CheckKind::TwoStarredExpressions => "TwoStarredExpressions",
|
||||
CheckKind::UndefinedExport(_) => "UndefinedExport",
|
||||
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
|
||||
CheckKind::UndefinedName(_) => "UndefinedName",
|
||||
@@ -234,7 +248,9 @@ impl CheckKind {
|
||||
CheckKind::NotIsTest => &CheckCode::E714,
|
||||
CheckKind::RaiseNotImplemented => &CheckCode::F901,
|
||||
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
|
||||
CheckKind::TooManyExpressionsInStarredAssignment => &CheckCode::F621,
|
||||
CheckKind::TrueFalseComparison(_, _) => &CheckCode::E712,
|
||||
CheckKind::TwoStarredExpressions => &CheckCode::F622,
|
||||
CheckKind::UndefinedExport(_) => &CheckCode::F822,
|
||||
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
|
||||
CheckKind::UndefinedName(_) => &CheckCode::F821,
|
||||
@@ -295,6 +311,9 @@ impl CheckKind {
|
||||
CheckKind::ReturnOutsideFunction => {
|
||||
"a `return` statement outside of a function/method".to_string()
|
||||
}
|
||||
CheckKind::TooManyExpressionsInStarredAssignment => {
|
||||
"too many expressions in star-unpacking assignment".to_string()
|
||||
}
|
||||
CheckKind::TrueFalseComparison(value, op) => match *value {
|
||||
true => match op {
|
||||
RejectedCmpop::Eq => {
|
||||
@@ -313,6 +332,7 @@ impl CheckKind {
|
||||
}
|
||||
},
|
||||
},
|
||||
CheckKind::TwoStarredExpressions => "two starred expressions in assignment".to_string(),
|
||||
CheckKind::UndefinedExport(name) => {
|
||||
format!("Undefined name `{name}` in `__all__`")
|
||||
}
|
||||
@@ -356,7 +376,9 @@ impl CheckKind {
|
||||
CheckKind::NoneComparison(_) => false,
|
||||
CheckKind::RaiseNotImplemented => false,
|
||||
CheckKind::ReturnOutsideFunction => false,
|
||||
CheckKind::TooManyExpressionsInStarredAssignment => false,
|
||||
CheckKind::TrueFalseComparison(_, _) => false,
|
||||
CheckKind::TwoStarredExpressions => false,
|
||||
CheckKind::UndefinedExport(_) => false,
|
||||
CheckKind::UndefinedLocal(_) => false,
|
||||
CheckKind::UndefinedName(_) => false,
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
extern crate core;
|
||||
|
||||
mod ast_ops;
|
||||
mod ast;
|
||||
mod autofix;
|
||||
mod builtins;
|
||||
mod cache;
|
||||
pub mod check_ast;
|
||||
mod check_lines;
|
||||
pub mod checks;
|
||||
mod fixer;
|
||||
pub mod fs;
|
||||
pub mod linter;
|
||||
pub mod logging;
|
||||
pub mod message;
|
||||
mod pyproject;
|
||||
mod relocator;
|
||||
mod python;
|
||||
pub mod settings;
|
||||
mod visitor;
|
||||
|
||||
@@ -4,15 +4,16 @@ use anyhow::Result;
|
||||
use log::debug;
|
||||
use rustpython_parser::parser;
|
||||
|
||||
use crate::autofix::fix_file;
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::fixer::fix_file;
|
||||
use crate::check_ast::check_ast;
|
||||
use crate::check_lines::check_lines;
|
||||
use crate::checks::{Check, LintSource};
|
||||
use crate::message::Message;
|
||||
use crate::settings::Settings;
|
||||
use crate::{autofix, cache, fs};
|
||||
use crate::{cache, fs};
|
||||
|
||||
fn check_path(path: &Path, settings: &Settings, autofix: &autofix::Mode) -> Result<Vec<Check>> {
|
||||
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
|
||||
// Read the file from disk.
|
||||
let contents = fs::read_file(path)?;
|
||||
|
||||
@@ -40,7 +41,7 @@ pub fn lint_path(
|
||||
path: &Path,
|
||||
settings: &Settings,
|
||||
mode: &cache::Mode,
|
||||
autofix: &autofix::Mode,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Vec<Message>> {
|
||||
let metadata = path.metadata()?;
|
||||
|
||||
@@ -57,7 +58,7 @@ pub fn lint_path(
|
||||
let mut checks = check_path(path, settings, autofix)?;
|
||||
|
||||
// Apply autofix.
|
||||
if matches!(autofix, autofix::Mode::Apply) {
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
fix_file(&mut checks, &contents, path)?;
|
||||
};
|
||||
|
||||
@@ -84,9 +85,10 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode, CheckKind, Fix, RejectedCmpop};
|
||||
use crate::linter::check_path;
|
||||
use crate::{autofix, settings};
|
||||
use crate::settings;
|
||||
|
||||
#[test]
|
||||
fn e402() -> Result<()> {
|
||||
@@ -97,7 +99,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E402]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![Check {
|
||||
@@ -122,7 +124,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E501]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![Check {
|
||||
@@ -147,7 +149,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E711]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -179,7 +181,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E712]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -222,7 +224,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E713]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![Check {
|
||||
@@ -247,7 +249,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E714]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![Check {
|
||||
@@ -272,7 +274,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E731]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -305,7 +307,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F401]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -342,7 +344,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F403]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -373,7 +375,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F541]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -410,7 +412,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F601]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![
|
||||
Check {
|
||||
@@ -446,7 +448,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F602]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![Check {
|
||||
kind: CheckKind::MultiValueRepeatedKeyVariable("a".to_string()),
|
||||
@@ -461,6 +463,30 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f622() -> Result<()> {
|
||||
let actual = check_path(
|
||||
Path::new("./resources/test/fixtures/F622.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F622]),
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![Check {
|
||||
kind: CheckKind::TwoStarredExpressions,
|
||||
location: Location::new(1, 1),
|
||||
fix: None,
|
||||
}];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
assert_eq!(actual[i], expected[i]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f631() -> Result<()> {
|
||||
let mut actual = check_path(
|
||||
@@ -470,7 +496,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F631]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -502,7 +528,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F634]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -534,7 +560,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F704]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -571,7 +597,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F706]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -603,7 +629,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F707]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -640,7 +666,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F821]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -687,7 +713,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F822]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![Check {
|
||||
@@ -712,7 +738,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F823]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![Check {
|
||||
@@ -737,7 +763,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F831]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -774,7 +800,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F841]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -806,18 +832,18 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F901]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
Check {
|
||||
kind: CheckKind::RaiseNotImplemented,
|
||||
location: Location::new(2, 5),
|
||||
location: Location::new(2, 25),
|
||||
fix: None,
|
||||
},
|
||||
Check {
|
||||
kind: CheckKind::RaiseNotImplemented,
|
||||
location: Location::new(6, 5),
|
||||
location: Location::new(6, 11),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
@@ -838,7 +864,7 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::R001]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
@@ -1060,13 +1086,13 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::R002]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
actual.sort_by_key(|check| check.location);
|
||||
let expected = vec![
|
||||
Check {
|
||||
kind: CheckKind::NoAssertEquals,
|
||||
location: Location::new(1, 19),
|
||||
location: Location::new(1, 5),
|
||||
fix: Some(Fix {
|
||||
content: "assertEqual".to_string(),
|
||||
start: Location::new(1, 6),
|
||||
@@ -1076,7 +1102,7 @@ mod tests {
|
||||
},
|
||||
Check {
|
||||
kind: CheckKind::NoAssertEquals,
|
||||
location: Location::new(2, 18),
|
||||
location: Location::new(2, 5),
|
||||
fix: Some(Fix {
|
||||
content: "assertEqual".to_string(),
|
||||
start: Location::new(2, 6),
|
||||
|
||||
11
src/main.rs
11
src/main.rs
@@ -6,6 +6,7 @@ use std::time::Instant;
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueHint};
|
||||
use colored::Colorize;
|
||||
use glob::Pattern;
|
||||
use log::{debug, error};
|
||||
use notify::{raw_watcher, RecursiveMode, Watcher};
|
||||
use rayon::prelude::*;
|
||||
@@ -47,12 +48,15 @@ struct Cli {
|
||||
/// Disable cache reads.
|
||||
#[clap(short, long, action)]
|
||||
no_cache: bool,
|
||||
/// Comma-separated list of error codes to enable.
|
||||
/// List of error codes to enable.
|
||||
#[clap(long, multiple = true)]
|
||||
select: Vec<CheckCode>,
|
||||
/// Comma-separated list of error codes to ignore.
|
||||
/// List of error codes to ignore.
|
||||
#[clap(long, multiple = true)]
|
||||
ignore: Vec<CheckCode>,
|
||||
/// List of file and/or directory patterns to exclude from checks.
|
||||
#[clap(long, multiple = true)]
|
||||
exclude: Vec<Pattern>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "update-informer")]
|
||||
@@ -188,6 +192,9 @@ fn inner_main() -> Result<ExitCode> {
|
||||
if !cli.ignore.is_empty() {
|
||||
settings.ignore(&cli.ignore);
|
||||
}
|
||||
if !cli.exclude.is_empty() {
|
||||
settings.exclude(cli.exclude);
|
||||
}
|
||||
|
||||
if cli.watch {
|
||||
if cli.fix {
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Ord for Message {
|
||||
(&self.filename, self.location.row(), self.location.column()).cmp(&(
|
||||
&other.filename,
|
||||
other.location.row(),
|
||||
self.location.column(),
|
||||
other.location.column(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,8 @@ other-attribute = 1
|
||||
CheckCode::F541,
|
||||
CheckCode::F601,
|
||||
CheckCode::F602,
|
||||
CheckCode::F621,
|
||||
CheckCode::F622,
|
||||
CheckCode::F631,
|
||||
CheckCode::F634,
|
||||
CheckCode::F704,
|
||||
|
||||
2
src/python.rs
Normal file
2
src/python.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod builtins;
|
||||
pub mod typing;
|
||||
@@ -156,8 +156,8 @@ pub const BUILTINS: &[&str] = &[
|
||||
// Globally defined names which are not attributes of the builtins module, or are only present on
|
||||
// some platforms.
|
||||
pub const MAGIC_GLOBALS: &[&str] = &[
|
||||
"__file__",
|
||||
"__builtins__",
|
||||
"__annotations__",
|
||||
"WindowsError",
|
||||
"__annotations__",
|
||||
"__builtins__",
|
||||
"__file__",
|
||||
];
|
||||
88
src/python/typing.rs
Normal file
88
src/python/typing.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
lazy_static! {
|
||||
static ref ANNOTATED_SUBSCRIPTS: BTreeSet<&'static str> = BTreeSet::from([
|
||||
"AbstractAsyncContextManager",
|
||||
"AbstractContextManager",
|
||||
"AbstractSet",
|
||||
"AsyncContextManager",
|
||||
"AsyncGenerator",
|
||||
"AsyncIterable",
|
||||
"AsyncIterator",
|
||||
"Awaitable",
|
||||
"BinaryIO",
|
||||
"BsdDbShelf",
|
||||
"ByteString",
|
||||
"Callable",
|
||||
"ChainMap",
|
||||
"ClassVar",
|
||||
"Collection",
|
||||
"Concatenate",
|
||||
"Container",
|
||||
"ContextManager",
|
||||
"Coroutine",
|
||||
"Counter",
|
||||
"Counter",
|
||||
"DbfilenameShelf",
|
||||
"DefaultDict",
|
||||
"Deque",
|
||||
"Dict",
|
||||
"Field",
|
||||
"Final",
|
||||
"FrozenSet",
|
||||
"Generator",
|
||||
"Iterator",
|
||||
"Generic",
|
||||
"IO",
|
||||
"ItemsView",
|
||||
"Iterable",
|
||||
"Iterator",
|
||||
"KeysView",
|
||||
"LifoQueue",
|
||||
"List",
|
||||
"Mapping",
|
||||
"MappingProxyType",
|
||||
"MappingView",
|
||||
"Match",
|
||||
"MutableMapping",
|
||||
"MutableSequence",
|
||||
"MutableSet",
|
||||
"Optional",
|
||||
"OrderedDict",
|
||||
"PathLike",
|
||||
"Pattern",
|
||||
"PriorityQueue",
|
||||
"Protocol",
|
||||
"Queue",
|
||||
"Reversible",
|
||||
"Sequence",
|
||||
"Set",
|
||||
"Shelf",
|
||||
"SimpleQueue",
|
||||
"TextIO",
|
||||
"Tuple",
|
||||
"Type",
|
||||
"TypeGuard",
|
||||
"Union",
|
||||
"ValuesView",
|
||||
"WeakKeyDictionary",
|
||||
"WeakMethod",
|
||||
"WeakSet",
|
||||
"WeakValueDictionary",
|
||||
"cached_property",
|
||||
"defaultdict",
|
||||
"deque",
|
||||
"dict",
|
||||
"frozenset",
|
||||
"list",
|
||||
"partialmethod",
|
||||
"set",
|
||||
"tuple",
|
||||
"type",
|
||||
]);
|
||||
}
|
||||
|
||||
pub fn is_annotated_subscript(name: &str) -> bool {
|
||||
ANNOTATED_SUBSCRIPTS.contains(name)
|
||||
}
|
||||
@@ -57,6 +57,8 @@ impl Settings {
|
||||
CheckCode::F541,
|
||||
CheckCode::F601,
|
||||
CheckCode::F602,
|
||||
CheckCode::F621,
|
||||
CheckCode::F622,
|
||||
CheckCode::F631,
|
||||
CheckCode::F634,
|
||||
CheckCode::F704,
|
||||
@@ -92,4 +94,8 @@ impl Settings {
|
||||
self.select.remove(code);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exclude(&mut self, exclude: Vec<Pattern>) {
|
||||
self.exclude = exclude;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user