Compare commits

...

17 Commits

Author SHA1 Message Date
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
Charlie Marsh
7c17785eac Bump version to 0.0.30 2022-09-08 11:42:45 -04:00
Charlie Marsh
f1acd28f08 Skip slice error for invalid TypeVar calls 2022-09-08 11:40:55 -04:00
Charlie Marsh
c61ff9a947 Adjust location when parsing deferred type annotations (#133) 2022-09-08 11:37:19 -04:00
Charlie Marsh
2c64cf3149 Add support for Literal, Type, and TypeVar (#131) 2022-09-08 11:07:45 -04:00
Charlie Marsh
fc5f34c76f Use scope-tracking logic for parents (#130) 2022-09-08 09:14:58 -04:00
Charlie Marsh
a8f4faa6e4 Fix crash on missing parent 2022-09-07 22:40:25 -04:00
Charlie Marsh
2ac5c830c1 Parse assignment annotations prior to targets (#127) 2022-09-07 22:35:39 -04:00
Charlie Marsh
994f12050d Support 'ignore' in pyproject.toml (#126) 2022-09-07 22:34:51 -04:00
Charlie Marsh
fad4e4c51d Defer checking of function bodies (#125) 2022-09-07 22:34:43 -04:00
Charlie Marsh
c0042a3ca4 Use Mode::None for --no-cache 2022-09-07 21:47:07 -04:00
Charlie Marsh
5deb63a05f Implement F601 and F602 (#122) 2022-09-07 12:57:50 -04:00
Charlie Marsh
5e9ea8bda2 Add documentation on parity with Flake8 2022-09-07 10:32:28 -04:00
26 changed files with 1700 additions and 898 deletions

View File

@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.29
rev: v0.0.31
hooks:
- id: lint

8
Cargo.lock generated
View File

@@ -1744,7 +1744,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.29"
version = "0.0.31"
dependencies = [
"anyhow",
"bincode",
@@ -1787,7 +1787,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 +1796,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 +1813,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.29"
version = "0.0.31"
edition = "2021"
[lib]
@@ -25,7 +25,7 @@ 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.29
rev: v0.0.31
hooks:
- id: lint
```
@@ -86,7 +86,7 @@ ruff path/to/code/ --select F401 F403
See `ruff --help` for more:
```shell
ruff (v0.0.29)
ruff (v0.0.31)
An extremely fast Python linter.
USAGE:
@@ -116,6 +116,31 @@ compatible out-of-the-box as long as the `line-length` setting is consistent bet
As a project, ruff is designed to be used alongside Black and, as such, will defer implementing
lint rules that are obviated by Black (e.g., stylistic rules).
### Parity with Flake8
ruff's goal is to achieve feature-parity with Flake8 when used (1) without any plugins,
(2) alongside Black, and (3) on Python 3 code. (Using Black obviates the need for many of Flake8's
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:
unused imports, undefined variables, etc.)
Of the unimplemented rules, ruff is missing:
- 15 rules related to string `.format` calls.
- 6 rules related to misplaced `yield`, `break`, and `return` statements.
- 3 rules related to syntax errors in doctests and annotations.
- 2 rules related to redefining unused names.
...along with a variety of others that don't fit neatly into any specific category.
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
1. Flake8 supports a wider range of `noqa` patterns, such as per-file ignores defined in `.flake8`.
2. Flake8 has a plugin architecture and supports writing custom lint rules.
3. ruff does not yet support parenthesized context managers.
## Rules
| Code | Name | Message |
@@ -131,6 +156,8 @@ lint rules that are obviated by Black (e.g., stylistic rules).
| F401 | UnusedImport | `...` imported but unused |
| F403 | ImportStarUsage | Unable to detect undefined names |
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated |
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated |
| 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 |

View File

@@ -13,6 +13,8 @@ fn main() {
CheckKind::ImportStarUsage,
CheckKind::LineTooLong,
CheckKind::ModuleImportNotAtTopOfFile,
CheckKind::MultiValueRepeatedKeyLiteral,
CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckKind::NoAssertEquals,
CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckKind::NotInTest,

View File

@@ -1,6 +1,7 @@
from __future__ import all_feature_names
import os
import functools
from datetime import datetime
from collections import (
Counter,
OrderedDict,
@@ -10,11 +11,15 @@ import multiprocessing.pool
import multiprocessing.process
import logging.config
import logging.handlers
from typing import NamedTuple, Dict, Type, TypeVar, List, Set
from blah import ClassA, ClassB, ClassC
class X:
datetime: datetime
foo: Type["NamedTuple"]
def a(self) -> "namedtuple":
x = os.environ["1"]
y = Counter()
@@ -23,3 +28,7 @@ class X:
__all__ = ["ClassA"] + ["ClassB"]
__all__ += ["ClassC"]
X = TypeVar("X")
Y = TypeVar("Y", bound="Dict")
Z = TypeVar("Z", "List", "Set")

12
resources/test/fixtures/F601.py vendored Normal file
View File

@@ -0,0 +1,12 @@
x = {
"a": 1,
"a": 2,
"b": 3,
("a", "b"): 3,
("a", "b"): 4,
1.0: 2,
1: 0,
1: 3,
b"123": 1,
b"123": 4,
}

7
resources/test/fixtures/F602.py vendored Normal file
View File

@@ -0,0 +1,7 @@
a = 1
b = 2
x = {
a: 1,
a: 2,
b: 3,
}

View File

@@ -36,10 +36,15 @@ class Foo:
ANNOTATED_CLASS_VAR: int = 2
from typing import Literal
class Class:
copy_on_model_validation: Literal["none", "deep", "shallow"]
post_init_call: Literal["before_validation", "after_validation"]
def __init__(self):
# TODO(charlie): This should be recognized as a defined variable.
Class # noqa: F821
Class
try:
@@ -49,3 +54,14 @@ except Exception as e:
y: int = 1
x: "Bar" = 1
[first] = ["yup"]
from typing import List, TypedDict
class Item(TypedDict):
nodes: List[TypedDict("Node", {"id": str})]

View File

@@ -13,6 +13,8 @@ select = [
"F401",
"F403",
"F541",
"F601",
"F602",
"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;

397
src/ast/checks.rs Normal file
View File

@@ -0,0 +1,397 @@
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
}

View File

@@ -1,56 +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)
}
pub enum ScopeKind {
Class,
Function,
Generator,
Module,
}
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> {

137
src/ast/relocate.rs Normal file
View File

@@ -0,0 +1,137 @@
use rustpython_parser::ast::{Expr, ExprKind, Keyword, Location};
fn relocate_keyword(keyword: &mut Keyword, location: Location) {
keyword.location = location;
relocate_expr(&mut keyword.node.value, location);
}
/// Change an expression's location (recursively) to match a desired, fixed location.
pub fn relocate_expr(expr: &mut Expr, location: Location) {
expr.location = location;
match &mut expr.node {
ExprKind::BoolOp { values, .. } => {
for expr in values {
relocate_expr(expr, location);
}
}
ExprKind::NamedExpr { target, value } => {
relocate_expr(target, location);
relocate_expr(value, location);
}
ExprKind::BinOp { left, right, .. } => {
relocate_expr(left, location);
relocate_expr(right, location);
}
ExprKind::UnaryOp { operand, .. } => {
relocate_expr(operand, location);
}
ExprKind::Lambda { body, .. } => {
relocate_expr(body, location);
}
ExprKind::IfExp { test, body, orelse } => {
relocate_expr(test, location);
relocate_expr(body, location);
relocate_expr(orelse, location);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
relocate_expr(expr, location);
}
for expr in values {
relocate_expr(expr, location);
}
}
ExprKind::Set { elts } => {
for expr in elts {
relocate_expr(expr, location);
}
}
ExprKind::ListComp { elt, .. } => {
relocate_expr(elt, location);
}
ExprKind::SetComp { elt, .. } => {
relocate_expr(elt, location);
}
ExprKind::DictComp { key, value, .. } => {
relocate_expr(key, location);
relocate_expr(value, location);
}
ExprKind::GeneratorExp { elt, .. } => {
relocate_expr(elt, location);
}
ExprKind::Await { value } => relocate_expr(value, location),
ExprKind::Yield { value } => {
if let Some(expr) = value {
relocate_expr(expr, location);
}
}
ExprKind::YieldFrom { value } => relocate_expr(value, location),
ExprKind::Compare {
left, comparators, ..
} => {
relocate_expr(left, location);
for expr in comparators {
relocate_expr(expr, location);
}
}
ExprKind::Call {
func,
args,
keywords,
} => {
relocate_expr(func, location);
for expr in args {
relocate_expr(expr, location);
}
for keyword in keywords {
relocate_keyword(keyword, location);
}
}
ExprKind::FormattedValue {
value, format_spec, ..
} => {
relocate_expr(value, location);
if let Some(expr) = format_spec {
relocate_expr(expr, location);
}
}
ExprKind::JoinedStr { values } => {
for expr in values {
relocate_expr(expr, location);
}
}
ExprKind::Constant { .. } => {}
ExprKind::Attribute { value, .. } => {
relocate_expr(value, location);
}
ExprKind::Subscript { value, slice, .. } => {
relocate_expr(value, location);
relocate_expr(slice, location);
}
ExprKind::Starred { value, .. } => {
relocate_expr(value, location);
}
ExprKind::Name { .. } => {}
ExprKind::List { elts, .. } => {
for expr in elts {
relocate_expr(expr, location);
}
}
ExprKind::Tuple { elts, .. } => {
for expr in elts {
relocate_expr(expr, location);
}
}
ExprKind::Slice { lower, upper, step } => {
if let Some(expr) = lower {
relocate_expr(expr, location);
}
if let Some(expr) = upper {
relocate_expr(expr, location);
}
if let Some(expr) = step {
relocate_expr(expr, location);
}
}
}
}

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

@@ -4,102 +4,141 @@ use rustpython_parser::ast::{
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
};
pub trait Visitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
pub trait Visitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
walk_stmt(self, stmt);
}
fn visit_annotation(&mut self, expr: &Expr) {
fn visit_annotation(&mut self, expr: &'a Expr) {
walk_expr(self, expr);
}
fn visit_expr(&mut self, expr: &Expr, _parent: Option<&Stmt>) {
fn visit_expr(&mut self, expr: &'a Expr) {
walk_expr(self, expr);
}
fn visit_constant(&mut self, constant: &Constant) {
fn visit_constant(&mut self, constant: &'a Constant) {
walk_constant(self, constant);
}
fn visit_expr_context(&mut self, expr_content: &ExprContext) {
fn visit_expr_context(&mut self, expr_content: &'a ExprContext) {
walk_expr_context(self, expr_content);
}
fn visit_boolop(&mut self, boolop: &Boolop) {
fn visit_boolop(&mut self, boolop: &'a Boolop) {
walk_boolop(self, boolop);
}
fn visit_operator(&mut self, operator: &Operator) {
fn visit_operator(&mut self, operator: &'a Operator) {
walk_operator(self, operator);
}
fn visit_unaryop(&mut self, unaryop: &Unaryop) {
fn visit_unaryop(&mut self, unaryop: &'a Unaryop) {
walk_unaryop(self, unaryop);
}
fn visit_cmpop(&mut self, cmpop: &Cmpop) {
fn visit_cmpop(&mut self, cmpop: &'a Cmpop) {
walk_cmpop(self, cmpop);
}
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
fn visit_comprehension(&mut self, comprehension: &'a Comprehension) {
walk_comprehension(self, comprehension);
}
fn visit_excepthandler(&mut self, excepthandler: &Excepthandler) {
fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) {
walk_excepthandler(self, excepthandler);
}
fn visit_arguments(&mut self, arguments: &Arguments) {
fn visit_arguments(&mut self, arguments: &'a Arguments) {
walk_arguments(self, arguments);
}
fn visit_arg(&mut self, arg: &Arg) {
fn visit_arg(&mut self, arg: &'a Arg) {
walk_arg(self, arg);
}
fn visit_keyword(&mut self, keyword: &Keyword) {
fn visit_keyword(&mut self, keyword: &'a Keyword) {
walk_keyword(self, keyword);
}
fn visit_alias(&mut self, alias: &Alias) {
fn visit_alias(&mut self, alias: &'a Alias) {
walk_alias(self, alias);
}
fn visit_withitem(&mut self, withitem: &Withitem) {
fn visit_withitem(&mut self, withitem: &'a Withitem) {
walk_withitem(self, withitem);
}
fn visit_match_case(&mut self, match_case: &MatchCase) {
fn visit_match_case(&mut self, match_case: &'a MatchCase) {
walk_match_case(self, match_case);
}
fn visit_pattern(&mut self, pattern: &Pattern) {
fn visit_pattern(&mut self, pattern: &'a Pattern) {
walk_pattern(self, pattern);
}
}
pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) {
match &stmt.node {
StmtKind::FunctionDef { args, body, .. } => {
StmtKind::FunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
visitor.visit_arguments(args);
for expr in decorator_list {
visitor.visit_expr(expr);
}
for expr in returns {
visitor.visit_annotation(expr);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::AsyncFunctionDef { args, body, .. } => {
StmtKind::AsyncFunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
visitor.visit_arguments(args);
for expr in decorator_list {
visitor.visit_expr(expr);
}
for expr in returns {
visitor.visit_annotation(expr);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::ClassDef { body, .. } => {
StmtKind::ClassDef {
bases,
keywords,
body,
decorator_list,
..
} => {
for expr in bases {
visitor.visit_expr(expr);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
}
for expr in decorator_list {
visitor.visit_expr(expr);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::Return { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
}
StmtKind::Delete { targets } => {
for expr in targets {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
}
StmtKind::Assign { targets, value, .. } => {
visitor.visit_expr(value);
for expr in targets {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
visitor.visit_expr(value, Some(stmt))
}
StmtKind::AugAssign { target, op, value } => {
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(target);
visitor.visit_operator(op);
visitor.visit_expr(value, Some(stmt));
visitor.visit_expr(value);
}
StmtKind::AnnAssign {
target,
@@ -107,11 +146,11 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
value,
..
} => {
visitor.visit_expr(target, Some(stmt));
visitor.visit_annotation(annotation);
if let Some(expr) = value {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
visitor.visit_expr(target);
}
StmtKind::For {
target,
@@ -120,13 +159,13 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
visitor.visit_expr(target);
visitor.visit_expr(iter);
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::AsyncFor {
@@ -136,31 +175,31 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
visitor.visit_expr(target);
visitor.visit_expr(iter);
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::While { test, body, orelse } => {
visitor.visit_expr(test, Some(stmt));
visitor.visit_expr(test);
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::If { test, body, orelse } => {
visitor.visit_expr(test, Some(stmt));
visitor.visit_expr(test);
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::With { items, body, .. } => {
@@ -168,7 +207,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
visitor.visit_withitem(withitem);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::AsyncWith { items, body, .. } => {
@@ -176,22 +215,22 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
visitor.visit_withitem(withitem);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::Match { subject, cases } => {
// TODO(charlie): Handle `cases`.
visitor.visit_expr(subject, Some(stmt));
visitor.visit_expr(subject);
for match_case in cases {
visitor.visit_match_case(match_case);
}
}
StmtKind::Raise { exc, cause } => {
if let Some(expr) = exc {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
};
if let Some(expr) = cause {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
};
}
StmtKind::Try {
@@ -201,22 +240,22 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
finalbody,
} => {
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for excepthandler in handlers {
visitor.visit_excepthandler(excepthandler)
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in finalbody {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::Assert { test, msg } => {
visitor.visit_expr(test, None);
visitor.visit_expr(test);
if let Some(expr) = msg {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
}
StmtKind::Import { names } => {
@@ -231,67 +270,67 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
StmtKind::Global { .. } => {}
StmtKind::Nonlocal { .. } => {}
StmtKind::Expr { value } => visitor.visit_expr(value, Some(stmt)),
StmtKind::Expr { value } => visitor.visit_expr(value),
StmtKind::Pass => {}
StmtKind::Break => {}
StmtKind::Continue => {}
}
}
pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
match &expr.node {
ExprKind::BoolOp { op, values } => {
visitor.visit_boolop(op);
for expr in values {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::NamedExpr { target, value } => {
visitor.visit_expr(target, None);
visitor.visit_expr(value, None);
visitor.visit_expr(target);
visitor.visit_expr(value);
}
ExprKind::BinOp { left, op, right } => {
visitor.visit_expr(left, None);
visitor.visit_expr(left);
visitor.visit_operator(op);
visitor.visit_expr(right, None);
visitor.visit_expr(right);
}
ExprKind::UnaryOp { op, operand } => {
visitor.visit_unaryop(op);
visitor.visit_expr(operand, None);
visitor.visit_expr(operand);
}
ExprKind::Lambda { args, body } => {
visitor.visit_arguments(args);
visitor.visit_expr(body, None);
visitor.visit_expr(body);
}
ExprKind::IfExp { test, body, orelse } => {
visitor.visit_expr(test, None);
visitor.visit_expr(body, None);
visitor.visit_expr(orelse, None);
visitor.visit_expr(test);
visitor.visit_expr(body);
visitor.visit_expr(orelse);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
for expr in values {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::Set { elts } => {
for expr in elts {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::ListComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt, None);
visitor.visit_expr(elt);
}
ExprKind::SetComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt, None);
visitor.visit_expr(elt);
}
ExprKind::DictComp {
key,
@@ -299,35 +338,35 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
generators,
} => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(key, None);
visitor.visit_expr(value, None);
visitor.visit_expr(key);
visitor.visit_expr(value);
}
ExprKind::GeneratorExp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt, None);
visitor.visit_expr(elt);
}
ExprKind::Await { value } => visitor.visit_expr(value, None),
ExprKind::Await { value } => visitor.visit_expr(value),
ExprKind::Yield { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::YieldFrom { value } => visitor.visit_expr(value, None),
ExprKind::YieldFrom { value } => visitor.visit_expr(value),
ExprKind::Compare {
left,
ops,
comparators,
} => {
visitor.visit_expr(left, None);
visitor.visit_expr(left);
for cmpop in ops {
visitor.visit_cmpop(cmpop);
}
for expr in comparators {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::Call {
@@ -335,9 +374,9 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
args,
keywords,
} => {
visitor.visit_expr(func, None);
visitor.visit_expr(func);
for expr in args {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
@@ -346,28 +385,28 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
ExprKind::FormattedValue {
value, format_spec, ..
} => {
visitor.visit_expr(value, None);
visitor.visit_expr(value);
if let Some(expr) = format_spec {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::JoinedStr { values } => {
for expr in values {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::Constant { value, .. } => visitor.visit_constant(value),
ExprKind::Attribute { value, ctx, .. } => {
visitor.visit_expr(value, None);
visitor.visit_expr(value);
visitor.visit_expr_context(ctx);
}
ExprKind::Subscript { value, slice, ctx } => {
visitor.visit_expr(value, None);
visitor.visit_expr(slice, None);
visitor.visit_expr(value);
visitor.visit_expr(slice);
visitor.visit_expr_context(ctx);
}
ExprKind::Starred { value, ctx } => {
visitor.visit_expr(value, None);
visitor.visit_expr(value);
visitor.visit_expr_context(ctx);
}
ExprKind::Name { ctx, .. } => {
@@ -375,31 +414,31 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
}
ExprKind::List { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Tuple { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Slice { lower, upper, step } => {
if let Some(expr) = lower {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
if let Some(expr) = upper {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
if let Some(expr) = step {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
}
}
}
pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant) {
pub fn walk_constant<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, constant: &'a Constant) {
if let Constant::Tuple(constants) = constant {
for constant in constants {
visitor.visit_constant(constant)
@@ -407,19 +446,25 @@ pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant)
}
}
pub fn walk_comprehension<V: Visitor + ?Sized>(visitor: &mut V, comprehension: &Comprehension) {
visitor.visit_expr(&comprehension.target, None);
visitor.visit_expr(&comprehension.iter, None);
pub fn walk_comprehension<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
comprehension: &'a Comprehension,
) {
visitor.visit_expr(&comprehension.target);
visitor.visit_expr(&comprehension.iter);
for expr in &comprehension.ifs {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
}
pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &Excepthandler) {
pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
excepthandler: &'a Excepthandler,
) {
match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { type_, body, .. } => {
if let Some(expr) = type_ {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
for stmt in body {
visitor.visit_stmt(stmt);
@@ -428,7 +473,7 @@ pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &
}
}
pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Arguments) {
pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) {
for arg in &arguments.posonlyargs {
visitor.visit_arg(arg);
}
@@ -436,61 +481,61 @@ pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Argument
visitor.visit_arg(arg);
}
if let Some(arg) = &arguments.vararg {
visitor.visit_arg(arg)
visitor.visit_arg(arg);
}
for arg in &arguments.kwonlyargs {
visitor.visit_arg(arg);
}
for expr in &arguments.kw_defaults {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
if let Some(arg) = &arguments.kwarg {
visitor.visit_arg(arg)
visitor.visit_arg(arg);
}
for expr in &arguments.defaults {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
pub fn walk_arg<V: Visitor + ?Sized>(visitor: &mut V, arg: &Arg) {
pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a Arg) {
if let Some(expr) = &arg.node.annotation {
visitor.visit_annotation(expr)
visitor.visit_annotation(expr);
}
}
pub fn walk_keyword<V: Visitor + ?Sized>(visitor: &mut V, keyword: &Keyword) {
visitor.visit_expr(&keyword.node.value, None);
pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a Keyword) {
visitor.visit_expr(&keyword.node.value);
}
pub fn walk_withitem<V: Visitor + ?Sized>(visitor: &mut V, withitem: &Withitem) {
visitor.visit_expr(&withitem.context_expr, None);
pub fn walk_withitem<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, withitem: &'a Withitem) {
visitor.visit_expr(&withitem.context_expr);
if let Some(expr) = &withitem.optional_vars {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
}
pub fn walk_match_case<V: Visitor + ?Sized>(visitor: &mut V, match_case: &MatchCase) {
pub fn walk_match_case<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, match_case: &'a MatchCase) {
visitor.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
for stmt in &match_case.body {
visitor.visit_stmt(stmt);
}
}
pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
pub fn walk_pattern<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, pattern: &'a Pattern) {
match &pattern.node {
PatternKind::MatchValue { value } => visitor.visit_expr(value, None),
PatternKind::MatchValue { value } => visitor.visit_expr(value),
PatternKind::MatchSingleton { value } => visitor.visit_constant(value),
PatternKind::MatchSequence { patterns } => {
for pattern in patterns {
visitor.visit_pattern(pattern)
visitor.visit_pattern(pattern);
}
}
PatternKind::MatchMapping { keys, patterns, .. } => {
for expr in keys {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
for pattern in patterns {
visitor.visit_pattern(pattern);
@@ -502,7 +547,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
kwd_patterns,
..
} => {
visitor.visit_expr(cls, None);
visitor.visit_expr(cls);
for pattern in patterns {
visitor.visit_pattern(pattern);
}
@@ -514,7 +559,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
PatternKind::MatchStar { .. } => {}
PatternKind::MatchAs { pattern, .. } => {
if let Some(pattern) = pattern {
visitor.visit_pattern(pattern)
visitor.visit_pattern(pattern);
}
}
PatternKind::MatchOr { patterns } => {
@@ -527,24 +572,28 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_expr_context<V: Visitor + ?Sized>(visitor: &mut V, expr_context: &ExprContext) {}
pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
expr_context: &'a ExprContext,
) {
}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_boolop<V: Visitor + ?Sized>(visitor: &mut V, boolop: &Boolop) {}
pub fn walk_boolop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, boolop: &'a Boolop) {}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_operator<V: Visitor + ?Sized>(visitor: &mut V, operator: &Operator) {}
pub fn walk_operator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, operator: &'a Operator) {}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_unaryop<V: Visitor + ?Sized>(visitor: &mut V, unaryop: &Unaryop) {}
pub fn walk_unaryop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unaryop: &'a Unaryop) {}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_cmpop<V: Visitor + ?Sized>(visitor: &mut V, cmpop: &Cmpop) {}
pub fn walk_cmpop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmpop: &'a Cmpop) {}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_alias<V: Visitor + ?Sized>(visitor: &mut V, alias: &Alias) {}
pub fn walk_alias<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, alias: &'a Alias) {}

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

@@ -1,8 +1,8 @@
use crate::ast_ops::SourceCodeLocator;
use rustpython_parser::ast::{Expr, Keyword, Location};
use rustpython_parser::lexer;
use rustpython_parser::token::Tok;
use crate::ast::operations::SourceCodeLocator;
use crate::checks::Fix;
/// Convert a location within a file (relative to `base`) to an absolute position.

View File

@@ -3,12 +3,12 @@ use std::fs::Metadata;
use std::hash::{Hash, Hasher};
use std::path::Path;
use crate::autofix;
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
use serde::{Deserialize, Serialize};
use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::Settings;
@@ -62,7 +62,7 @@ impl From<bool> for Mode {
fn from(value: bool) -> Self {
match value {
true => Mode::ReadWrite,
false => Mode::WriteOnly,
false => Mode::None,
}
}
}
@@ -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,
) {

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,8 @@ pub enum CheckCode {
F401,
F403,
F541,
F601,
F602,
F631,
F634,
F704,
@@ -50,6 +52,8 @@ impl FromStr for CheckCode {
"F401" => Ok(CheckCode::F401),
"F403" => Ok(CheckCode::F403),
"F541" => Ok(CheckCode::F541),
"F601" => Ok(CheckCode::F601),
"F602" => Ok(CheckCode::F602),
"F631" => Ok(CheckCode::F631),
"F634" => Ok(CheckCode::F634),
"F704" => Ok(CheckCode::F704),
@@ -82,6 +86,8 @@ impl CheckCode {
CheckCode::F401 => "F401",
CheckCode::F403 => "F403",
CheckCode::F541 => "F541",
CheckCode::F601 => "F601",
CheckCode::F602 => "F602",
CheckCode::F631 => "F631",
CheckCode::F634 => "F634",
CheckCode::F704 => "F704",
@@ -112,6 +118,8 @@ impl CheckCode {
CheckCode::F401 => &LintSource::AST,
CheckCode::F403 => &LintSource::AST,
CheckCode::F541 => &LintSource::AST,
CheckCode::F601 => &LintSource::AST,
CheckCode::F602 => &LintSource::AST,
CheckCode::F631 => &LintSource::AST,
CheckCode::F634 => &LintSource::AST,
CheckCode::F704 => &LintSource::AST,
@@ -154,6 +162,8 @@ pub enum CheckKind {
ImportStarUsage,
LineTooLong,
ModuleImportNotAtTopOfFile,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable(String),
NoAssertEquals,
NoneComparison(RejectedCmpop),
NotInTest,
@@ -184,6 +194,8 @@ impl CheckKind {
CheckKind::LineTooLong => "LineTooLong",
CheckKind::DoNotAssignLambda => "DoNotAssignLambda",
CheckKind::ModuleImportNotAtTopOfFile => "ModuleImportNotAtTopOfFile",
CheckKind::MultiValueRepeatedKeyLiteral => "MultiValueRepeatedKeyLiteral",
CheckKind::MultiValueRepeatedKeyVariable(_) => "MultiValueRepeatedKeyVariable",
CheckKind::NoAssertEquals => "NoAssertEquals",
CheckKind::NoneComparison(_) => "NoneComparison",
CheckKind::NotInTest => "NotInTest",
@@ -214,6 +226,8 @@ impl CheckKind {
CheckKind::LineTooLong => &CheckCode::E501,
CheckKind::DoNotAssignLambda => &CheckCode::E731,
CheckKind::ModuleImportNotAtTopOfFile => &CheckCode::E402,
CheckKind::MultiValueRepeatedKeyLiteral => &CheckCode::F601,
CheckKind::MultiValueRepeatedKeyVariable(_) => &CheckCode::F602,
CheckKind::NoAssertEquals => &CheckCode::R002,
CheckKind::NoneComparison(_) => &CheckCode::E711,
CheckKind::NotInTest => &CheckCode::E713,
@@ -258,6 +272,12 @@ impl CheckKind {
CheckKind::ModuleImportNotAtTopOfFile => {
"Module level import not at top of file".to_string()
}
CheckKind::MultiValueRepeatedKeyLiteral => {
"Dictionary key literal repeated".to_string()
}
CheckKind::MultiValueRepeatedKeyVariable(name) => {
format!("Dictionary key `{name}` repeated")
}
CheckKind::NoAssertEquals => {
"`assertEquals` is deprecated, use `assertEqual` instead".to_string()
}
@@ -328,6 +348,8 @@ impl CheckKind {
CheckKind::DoNotAssignLambda => false,
CheckKind::LineTooLong => false,
CheckKind::ModuleImportNotAtTopOfFile => false,
CheckKind::MultiValueRepeatedKeyLiteral => false,
CheckKind::MultiValueRepeatedKeyVariable(_) => false,
CheckKind::NoAssertEquals => true,
CheckKind::NotInTest => false,
CheckKind::NotIsTest => false,

View File

@@ -1,17 +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;
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,21 +85,23 @@ 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<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E402]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![Check {
kind: CheckKind::ModuleImportNotAtTopOfFile,
location: Location::new(20, 1),
@@ -114,15 +117,16 @@ mod tests {
#[test]
fn e501() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/E501.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E501]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![Check {
kind: CheckKind::LineTooLong,
location: Location::new(5, 89),
@@ -138,15 +142,16 @@ mod tests {
#[test]
fn e711() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/E711.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E711]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::NoneComparison(RejectedCmpop::Eq),
@@ -169,15 +174,16 @@ mod tests {
#[test]
fn e712() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/E712.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E712]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
@@ -211,15 +217,16 @@ mod tests {
#[test]
fn e713() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/E713.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E713]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![Check {
kind: CheckKind::NotInTest,
location: Location::new(2, 12),
@@ -235,15 +242,16 @@ mod tests {
#[test]
fn e714() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/E714.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E714]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![Check {
kind: CheckKind::NotIsTest,
location: Location::new(1, 13),
@@ -259,15 +267,16 @@ mod tests {
#[test]
fn e731() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/E731.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E731]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::DoNotAssignLambda,
@@ -291,21 +300,17 @@ mod tests {
#[test]
fn f401() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F401.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F401]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::UnusedImport("logging.handlers".to_string()),
location: Location::new(12, 1),
fix: None,
},
Check {
kind: CheckKind::UnusedImport("functools".to_string()),
location: Location::new(3, 1),
@@ -313,7 +318,12 @@ mod tests {
},
Check {
kind: CheckKind::UnusedImport("collections.OrderedDict".to_string()),
location: Location::new(4, 1),
location: Location::new(5, 1),
fix: None,
},
Check {
kind: CheckKind::UnusedImport("logging.handlers".to_string()),
location: Location::new(13, 1),
fix: None,
},
];
@@ -327,15 +337,16 @@ mod tests {
#[test]
fn f403() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F403.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F403]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::ImportStarUsage,
@@ -357,15 +368,16 @@ mod tests {
}
#[test]
fn f541() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F541.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F541]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::FStringMissingPlaceholders,
@@ -392,16 +404,77 @@ mod tests {
}
#[test]
fn f631() -> Result<()> {
fn f601() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/F601.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F601]),
},
&fixer::Mode::Generate,
)?;
let expected = vec![
Check {
kind: CheckKind::MultiValueRepeatedKeyLiteral,
location: Location::new(3, 6),
fix: None,
},
Check {
kind: CheckKind::MultiValueRepeatedKeyLiteral,
location: Location::new(9, 5),
fix: None,
},
Check {
kind: CheckKind::MultiValueRepeatedKeyLiteral,
location: Location::new(11, 7),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f602() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/F602.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F602]),
},
&fixer::Mode::Generate,
)?;
let expected = vec![Check {
kind: CheckKind::MultiValueRepeatedKeyVariable("a".to_string()),
location: Location::new(5, 5),
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(
Path::new("./resources/test/fixtures/F631.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F631]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::AssertTuple,
@@ -424,15 +497,16 @@ mod tests {
#[test]
fn f634() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F634.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F634]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::IfTuple,
@@ -455,15 +529,16 @@ mod tests {
#[test]
fn f704() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F704.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F704]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::YieldOutsideFunction,
@@ -491,15 +566,16 @@ mod tests {
#[test]
fn f706() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F706.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F706]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::ReturnOutsideFunction,
@@ -522,15 +598,16 @@ mod tests {
#[test]
fn f707() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F707.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F707]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::DefaultExceptNotLast,
@@ -558,15 +635,16 @@ mod tests {
#[test]
fn f821() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F821.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F821]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::UndefinedName("self".to_string()),
@@ -588,6 +666,11 @@ mod tests {
location: Location::new(21, 12),
fix: None,
},
Check {
kind: CheckKind::UndefinedName("Bar".to_string()),
location: Location::new(58, 5),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
@@ -599,15 +682,16 @@ mod tests {
#[test]
fn f822() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F822.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F822]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![Check {
kind: CheckKind::UndefinedExport("b".to_string()),
location: Location::new(3, 1),
@@ -623,15 +707,16 @@ mod tests {
#[test]
fn f823() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F823.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F823]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![Check {
kind: CheckKind::UndefinedLocal("my_var".to_string()),
location: Location::new(6, 5),
@@ -647,15 +732,16 @@ mod tests {
#[test]
fn f831() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F831.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F831]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::DuplicateArgumentName,
@@ -683,15 +769,16 @@ mod tests {
#[test]
fn f841() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F841]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::UnusedVariable("e".to_string()),
@@ -714,24 +801,25 @@ mod tests {
#[test]
fn f901() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/F901.py"),
&settings::Settings {
line_length: 88,
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,
},
];
@@ -745,15 +833,16 @@ mod tests {
#[test]
fn r001() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/R001.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::R001]),
},
&autofix::Mode::Generate,
&fixer::Mode::Generate,
)?;
actual.sort_by_key(|check| check.location);
let expected = vec![
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
@@ -966,19 +1055,20 @@ mod tests {
#[test]
fn r002() -> Result<()> {
let actual = check_path(
let mut actual = check_path(
Path::new("./resources/test/fixtures/R002.py"),
&settings::Settings {
line_length: 88,
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),
@@ -988,7 +1078,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

@@ -1,4 +1,3 @@
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use anyhow::Result;
@@ -40,7 +39,8 @@ pub fn load_config<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<(Pat
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<PathBuf>>,
pub select: Option<BTreeSet<CheckCode>>,
pub select: Option<Vec<CheckCode>>,
pub ignore: Option<Vec<CheckCode>>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
@@ -90,7 +90,6 @@ fn find_project_root<'a>(sources: impl IntoIterator<Item = &'a Path>) -> Option<
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::path::Path;
use anyhow::Result;
@@ -125,6 +124,7 @@ mod tests {
line_length: None,
exclude: None,
select: None,
ignore: None,
})
})
);
@@ -143,6 +143,7 @@ line-length = 79
line_length: Some(79),
exclude: None,
select: None,
ignore: None,
})
})
);
@@ -161,6 +162,7 @@ exclude = ["foo.py"]
line_length: None,
exclude: Some(vec![Path::new("foo.py").to_path_buf()]),
select: None,
ignore: None,
})
})
);
@@ -178,7 +180,27 @@ select = ["E501"]
ruff: Some(Config {
line_length: None,
exclude: None,
select: Some(BTreeSet::from([CheckCode::E501])),
select: Some(vec![CheckCode::E501]),
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
ignore = ["E501"]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: None,
select: None,
ignore: Some(vec![CheckCode::E501]),
})
})
);
@@ -236,7 +258,7 @@ other-attribute = 1
Path::new("excluded.py").to_path_buf(),
Path::new("**/migrations").to_path_buf()
]),
select: Some(BTreeSet::from([
select: Some(vec![
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
@@ -248,6 +270,8 @@ other-attribute = 1
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F631,
CheckCode::F634,
CheckCode::F704,
@@ -261,7 +285,8 @@ other-attribute = 1
CheckCode::F901,
CheckCode::R001,
CheckCode::R002,
])),
]),
ignore: None,
}
);

View File

@@ -27,7 +27,7 @@ impl Hash for Settings {
impl Settings {
pub fn from_paths<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<Self> {
let (project_root, config) = load_config(paths)?;
Ok(Settings {
let mut settings = Settings {
line_length: config.line_length.unwrap_or(88),
exclude: config
.exclude
@@ -42,8 +42,8 @@ impl Settings {
})
.map(|path| Pattern::new(&path.to_string_lossy()).expect("Invalid pattern."))
.collect(),
select: config.select.unwrap_or_else(|| {
BTreeSet::from([
select: BTreeSet::from_iter(config.select.unwrap_or_else(|| {
vec![
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
@@ -55,6 +55,8 @@ impl Settings {
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F631,
CheckCode::F634,
CheckCode::F704,
@@ -69,9 +71,13 @@ impl Settings {
// Disable refactoring codes by default.
// CheckCode::R001,
// CheckCode::R002,
])
}),
})
]
})),
};
if let Some(ignore) = &config.ignore {
settings.ignore(ignore);
}
Ok(settings)
}
pub fn select(&mut self, codes: Vec<CheckCode>) {