Compare commits

...

17 Commits

Author SHA1 Message Date
Charlie Marsh
81ae3bfc94 Bump version to 0.0.33 2022-09-11 10:45:02 -04:00
Charlie Marsh
62e6feadc7 Handle accesses within inner functions (#156) 2022-09-11 10:44:27 -04:00
Charlie Marsh
18a26e8f0b Allow setting --exclude on the command-line (#157) 2022-09-11 10:44:23 -04:00
Charlie Marsh
2371de3895 Make late imports more permissive (#155) 2022-09-11 10:44:06 -04:00
Charlie Marsh
e3c8f61340 Ignore deletes in conditional branches (#154) 2022-09-11 10:28:07 -04:00
Harutaka Kawamura
f6628ae100 Fix Message.cmp (#152) 2022-09-11 10:18:27 -04:00
Jakub Kuczys
989ed9c10b Fix ruff's pyproject.toml section name in README.md (#148) 2022-09-11 10:18:19 -04:00
Charlie Marsh
8698c06c36 Bump version to 0.0.32 2022-09-10 15:21:01 -04:00
Charlie Marsh
dfd8a4158d Parse function annotations within the ClassDef scope (#144) 2022-09-10 15:20:39 -04:00
Charlie Marsh
c247730bf5 Avoid treating keys as annotations in TypedDict 2022-09-10 15:19:11 -04:00
Charlie Marsh
024472d578 Implement F621 and F622 (#143) 2022-09-10 15:04:33 -04:00
Charlie Marsh
7d69a153e8 Support remaining typing module members (#141) 2022-09-10 14:51:43 -04:00
Charlie Marsh
4fc68e0310 Bump version to 0.0.31 2022-09-10 13:05:15 -04:00
Charlie Marsh
6a24351202 Add support for TypedDict 2022-09-10 13:03:29 -04:00
Charlie Marsh
d7f95ac6b6 Upgrade RustPython parser to handle list assignments 2022-09-10 12:53:07 -04:00
Charlie Marsh
11234ea555 Add await to YieldOutsideFunction 2022-09-08 22:54:11 -04:00
Charlie Marsh
b536159541 Pull check logic out of check_ast.rs (#135) 2022-09-08 22:46:42 -04:00
30 changed files with 1305 additions and 749 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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" }

View File

@@ -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",

View File

@@ -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()),

View File

@@ -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
View File

@@ -0,0 +1,3 @@
*a, *b, c = (1, 2, 3)
*a, b, c = (1, 2, 3)
a, b, *c = (1, 2, 3)

View File

@@ -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

View File

@@ -15,6 +15,8 @@ select = [
"F541",
"F601",
"F602",
"F621",
"F622",
"F631",
"F634",
"F704",

5
src/ast.rs Normal file
View 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
View 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
}

View File

@@ -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
View 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>,
}

View File

@@ -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
View 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(())
}
}

View File

@@ -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.

View File

@@ -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,
) {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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),

View File

@@ -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 {

View File

@@ -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(),
))
}
}

View File

@@ -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
View File

@@ -0,0 +1,2 @@
pub mod builtins;
pub mod typing;

View File

@@ -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
View 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)
}

View File

@@ -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;
}
}