Compare commits

..

23 Commits

Author SHA1 Message Date
Charlie Marsh
4319bd1755 Bump version to 0.0.25 2022-09-03 12:09:11 -04:00
Charlie Marsh
6bb6cb1783 Implement F822 (#94) 2022-09-03 12:08:26 -04:00
Charlie Marsh
e9412c9452 Generate a list of supported lint rules (#93) 2022-09-03 11:56:11 -04:00
Charlie Marsh
94faa7f301 Rename resources/test/src to resources/test/fixtures (#92) 2022-09-03 11:49:03 -04:00
Charlie Marsh
5041f6530c Implement R0205 (#91) 2022-09-03 11:33:54 -04:00
Narawit Rakket
3c7716ef27 refactor: run cargo clippy --fix (#88) 2022-09-02 17:01:24 -04:00
Charlie Marsh
26e1f4b6df Bump version to 0.0.24 2022-09-02 10:18:40 -04:00
Charlie Marsh
17c08523dc Remove rogue println 2022-09-02 10:18:12 -04:00
Charlie Marsh
221f4304ad Add support for __all__ export bindings (#87) 2022-09-02 10:17:31 -04:00
Charlie Marsh
c0131e65e5 Avoid putting decorators in the function scope (#86) 2022-09-02 09:13:06 -04:00
Charlie Marsh
bf4722a62f Fix future-to-__future__ typo (#85) 2022-09-02 08:56:26 -04:00
Charlie Marsh
994f5d452c Update .gitignore 2022-09-01 20:32:32 -04:00
Nikita Sobolev
741857cdf9 Use the latest version of actions/checkout (#79) 2022-09-01 13:18:31 -04:00
Ariel Richtman
4f42f51bd2 Add pre-commit hook (#55) 2022-09-01 13:01:28 -04:00
Dmitry Dygalo
5a3092e805 perf: Compile Regex once (#77) 2022-09-01 12:49:29 -04:00
Charlie Marsh
0318406535 Re-sort lint rules 2022-09-01 12:39:25 -04:00
Sekky61
0c99b5aac5 Fix F832 --> F823 typo (#73) 2022-09-01 12:37:34 -04:00
Kian-Meng Ang
b442402b13 Prettify md/yaml files (#74) 2022-09-01 12:36:47 -04:00
Charlie Marsh
ba27e50164 Bump version to 0.0.23 2022-09-01 09:21:43 -04:00
Charlie Marsh
d55651d470 Rename cache to .ruff_cache 2022-09-01 09:19:32 -04:00
Charlie Marsh
0c110bcecf Add some user-visible logging when pyproject.toml fails to parse 2022-09-01 09:18:47 -04:00
Patrick Haller
9bf8a0d0f1 Check if toml file is parsed correctly (#71) 2022-09-01 09:15:07 -04:00
Eric Hagman
888cfeba35 Remove .to_string() from F634 error message (#67) 2022-09-01 09:08:48 -04:00
41 changed files with 732 additions and 359 deletions

View File

@@ -2,16 +2,16 @@ name: CI
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
jobs:
cargo_build:
name: "cargo build"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -33,7 +33,7 @@ jobs:
name: "cargo fmt"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -55,7 +55,7 @@ jobs:
name: "cargo clippy"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -77,7 +77,7 @@ jobs:
name: "cargo test"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -99,13 +99,13 @@ jobs:
name: "maturin build"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: "3.10"
- run: pip install maturin
- uses: actions/cache@v3
env:

View File

@@ -227,9 +227,9 @@ jobs:
os: [ubuntu-latest, macos-latest]
target: [x86_64, aarch64]
python-version:
- '3.7'
- '3.8'
- '3.9'
- "3.7"
- "3.8"
- "3.9"
exclude:
- os: macos-latest
target: aarch64

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Local cache
.cache
.ruff_cache
resources/test/cpython
###

5
.pre-commit-config.yaml Normal file
View File

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

7
.pre-commit-hooks.yaml Normal file
View File

@@ -0,0 +1,7 @@
- id: lint
name: ruff lint
description: Run ruff to lint Python files.
entry: ruff
language: python
types_or: [python]
pass_filenames: true

7
Cargo.lock generated
View File

@@ -1260,9 +1260,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.13.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
[[package]]
name = "opaque-debug"
@@ -1641,7 +1641,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.22"
version = "0.0.25"
dependencies = [
"anyhow",
"bincode",
@@ -1657,6 +1657,7 @@ dependencies = [
"glob",
"log",
"notify",
"once_cell",
"rayon",
"regex",
"rustpython-parser",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.22"
version = "0.0.25"
edition = "2021"
[lib]
@@ -21,6 +21,7 @@ filetime = { version = "0.2.17" }
glob = { version = "0.3.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 = "1613f6c6990011a4bc559e79aaf28d715f9f729b" }

View File

@@ -51,6 +51,16 @@ You can run ruff in `--watch` mode to automatically re-run on-change:
ruff path/to/code/ --watch
```
ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on system):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.25
hooks:
- id: lint
```
## Configuration
ruff is configurable both via `pyproject.toml` and the command line.
@@ -96,6 +106,25 @@ OPTIONS:
-w, --watch Run in watch mode by re-running whenever files change
```
## Rules
| Code | Name | Message |
| ---- | ----- | ------- |
| E501 | LineTooLong | Line too long |
| F401 | UnusedImport | `...` imported but unused |
| F403 | ImportStarUsage | Unable to detect undefined names |
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
| F634 | IfTuple | If test is a tuple, which is always `True` |
| F704 | YieldOutsideFunction | a `yield` or `yield from` statement outside of a function/method |
| F706 | ReturnOutsideFunction | a `return` statement outside of a function/method |
| F821 | UndefinedName | Undefined name `...` |
| F822 | UndefinedExport | Undefined name `...` in __all__ |
| F823 | UndefinedLocal | Local variable `...` referenced before assignment |
| F831 | DuplicateArgumentName | Duplicate argument name in function definition |
| F841 | UnusedVariable | Local variable `...` is assigned to but never used |
| F901 | RaiseNotImplemented | 'raise NotImplemented' should be 'raise NotImplementedError |
| R0205 | UselessObjectInheritance | Class ... inherits from object |
## Development
ruff is written in Rust (1.63.0). You'll need to install the [Rust toolchain](https://www.rust-lang.org/tools/install)
@@ -104,7 +133,7 @@ for development.
Assuming you have `cargo` installed, you can run:
```shell
cargo run resources/test/src
cargo run resources/test/fixtures
cargo fmt
cargo clippy
cargo test
@@ -215,6 +244,7 @@ hyperfine --ignore-failure --warmup 5 \
```
In order, these evaluate:
- ruff
- Pylint
- PyFlakes

View File

@@ -0,0 +1,33 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::CheckKind;
fn main() {
let mut check_kinds: Vec<CheckKind> = vec![
CheckKind::DuplicateArgumentName,
CheckKind::FStringMissingPlaceholders,
CheckKind::IfTuple,
CheckKind::ImportStarUsage,
CheckKind::LineTooLong,
CheckKind::RaiseNotImplemented,
CheckKind::ReturnOutsideFunction,
CheckKind::UndefinedLocal("...".to_string()),
CheckKind::UndefinedName("...".to_string()),
CheckKind::UndefinedExport("...".to_string()),
CheckKind::UnusedImport("...".to_string()),
CheckKind::UnusedVariable("...".to_string()),
CheckKind::UselessObjectInheritance("...".to_string()),
CheckKind::YieldOutsideFunction,
];
check_kinds.sort_by_key(|check_kind| check_kind.code());
println!("| Code | Name | Message |");
println!("| ---- | ----- | ------- |");
for check_kind in check_kinds {
println!(
"| {} | {} | {} |",
check_kind.code().as_str(),
check_kind.name(),
check_kind.body()
);
}
}

View File

@@ -1,3 +1,4 @@
from __future__ import all_feature_names
import os
import functools
from collections import (
@@ -10,9 +11,15 @@ import multiprocessing.process
import logging.config
import logging.handlers
from blah import ClassA, ClassB, ClassC
class X:
def a(self) -> "namedtuple":
x = os.environ["1"]
y = Counter()
z = multiprocessing.pool.ThreadPool()
__all__ = ["ClassA"] + ["ClassB"]
__all__ += ["ClassC"]

3
resources/test/fixtures/F822.py vendored Normal file
View File

@@ -0,0 +1,3 @@
a = 1
__all__ = ["a", "b"]

View File

@@ -15,3 +15,13 @@ def baz():
global my_var
global my_dict
my_dict[my_var] += 1
def dec(x):
return x
@dec
def f():
dec = 1
return dec

22
resources/test/fixtures/R0205.py vendored Normal file
View File

@@ -0,0 +1,22 @@
class A:
...
class B(object):
...
class C(B, object):
...
def f():
class D(object):
...
object = A
class E(object):
...

View File

@@ -10,8 +10,10 @@ select = [
"F704",
"F706",
"F821",
"F822",
"F823",
"F831",
"F832",
"F841",
"F901",
"R0205",
]

117
src/ast_ops.rs Normal file
View File

@@ -0,0 +1,117 @@
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>,
}
/// Extract the names bound to a given __all__ assignment.
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
let mut names: Vec<String> = vec![];
fn add_to_names(names: &mut Vec<String>, elts: &[Expr]) {
for elt in elts {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &elt.node
{
names.push(value.to_string())
}
}
}
// Grab the existing bound __all__ values.
if let StmtKind::AugAssign { .. } = &stmt.node {
if let Some(binding) = scope.values.get("__all__") {
if let BindingKind::Export(existing) = &binding.kind {
names.extend(existing.clone());
}
}
}
if let Some(value) = match &stmt.node {
StmtKind::Assign { value, .. } => Some(value),
StmtKind::AnnAssign { value, .. } => value.as_ref(),
StmtKind::AugAssign { value, .. } => Some(value),
_ => None,
} {
match &value.node {
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
add_to_names(&mut names, elts)
}
ExprKind::BinOp { left, right, .. } => {
let mut current_left = left;
let mut current_right = right;
while let Some(elts) = match &current_right.node {
ExprKind::List { elts, .. } => Some(elts),
ExprKind::Tuple { elts, .. } => Some(elts),
_ => None,
} {
add_to_names(&mut names, elts);
match &current_left.node {
ExprKind::BinOp { left, right, .. } => {
current_left = left;
current_right = right;
}
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
add_to_names(&mut names, elts);
break;
}
_ => break,
}
}
}
_ => {}
}
}
names
}

View File

@@ -66,7 +66,7 @@ impl From<bool> for Mode {
}
fn cache_dir() -> &'static str {
"./.cache"
"./.ruff_cache"
}
fn cache_key(path: &Path, settings: &Settings) -> String {

View File

@@ -1,69 +1,22 @@
use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::collections::BTreeSet;
use std::path::Path;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
Location, Stmt, StmtKind, Suite,
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Stmt,
StmtKind, Suite,
};
use rustpython_parser::parser;
use crate::ast_ops::{extract_all_names, Binding, BindingKind, Scope, ScopeKind};
use crate::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::check_ast::ScopeKind::{Class, Function, Generator, Module};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::settings::Settings;
use crate::visitor;
use crate::visitor::{walk_excepthandler, Visitor};
fn id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
enum ScopeKind {
Class,
Function,
Generator,
Module,
}
struct Scope {
id: usize,
kind: ScopeKind,
values: BTreeMap<String, Binding>,
}
impl Scope {
fn new(kind: ScopeKind) -> Self {
Scope {
id: id(),
kind,
values: BTreeMap::new(),
}
}
}
#[derive(Clone)]
enum BindingKind {
Argument,
Assignment,
Definition,
ClassDefinition,
Builtin,
FutureImportation,
Importation(String),
StarImportation,
SubmoduleImportation(String),
}
#[derive(Clone)]
struct Binding {
kind: BindingKind,
location: Location,
used: Option<usize>,
}
struct Checker<'a> {
settings: &'a Settings,
path: &'a str,
checks: Vec<Check>,
scopes: Vec<Scope>,
dead_scopes: Vec<Scope>,
@@ -73,9 +26,10 @@ struct Checker<'a> {
}
impl Checker<'_> {
pub fn new(settings: &Settings) -> Checker {
pub fn new<'a>(settings: &'a Settings, path: &'a str) -> Checker<'a> {
Checker {
settings,
path,
checks: vec![],
scopes: vec![],
dead_scopes: vec![],
@@ -109,18 +63,24 @@ impl Visitor for Checker<'_> {
}
}
}
StmtKind::FunctionDef { name, .. } => {
self.add_binding(
name.to_string(),
Binding {
kind: BindingKind::Definition,
used: None,
location: stmt.location,
},
);
self.push_scope(Scope::new(Function));
StmtKind::FunctionDef {
name,
decorator_list,
returns,
..
}
StmtKind::AsyncFunctionDef { name, .. } => {
| StmtKind::AsyncFunctionDef {
name,
decorator_list,
returns,
..
} => {
for expr in decorator_list {
self.visit_expr(expr, Some(stmt));
}
for expr in returns {
self.visit_annotation(expr);
}
self.add_binding(
name.to_string(),
Binding {
@@ -129,7 +89,7 @@ impl Visitor for Checker<'_> {
location: stmt.location,
},
);
self.push_scope(Scope::new(Function));
self.push_scope(Scope::new(ScopeKind::Function));
}
StmtKind::Return { .. } => {
if self
@@ -139,7 +99,7 @@ impl Visitor for Checker<'_> {
{
if let Some(scope) = self.scopes.last() {
match scope.kind {
Class | Module => {
ScopeKind::Class | ScopeKind::Module => {
self.checks.push(Check {
kind: CheckKind::ReturnOutsideFunction,
location: stmt.location,
@@ -150,7 +110,49 @@ impl Visitor for Checker<'_> {
}
}
}
StmtKind::ClassDef { .. } => self.push_scope(Scope::new(Class)),
StmtKind::ClassDef {
name,
bases,
keywords,
decorator_list,
..
} => {
if self.settings.select.contains(&CheckCode::R0205) {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
if id == "object" {
let scope = self.scopes.last().expect("No current scope found.");
match scope.values.get(id) {
None
| Some(Binding {
kind: BindingKind::Builtin,
..
}) => {
self.checks.push(Check {
kind: CheckKind::UselessObjectInheritance(
name.to_string(),
),
location: stmt.location,
});
}
_ => {}
}
}
}
}
}
for expr in bases {
self.visit_expr(expr, Some(stmt))
}
for keyword in keywords {
self.visit_keyword(keyword)
}
for expr in decorator_list {
self.visit_expr(expr, Some(stmt))
}
self.push_scope(Scope::new(ScopeKind::Class))
}
StmtKind::Import { names } => {
for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() {
@@ -195,12 +197,11 @@ impl Visitor for Checker<'_> {
.asname
.clone()
.unwrap_or_else(|| alias.node.name.clone());
if let Some("future") = module.as_deref() {
if let Some("__future__") = module.as_deref() {
self.add_binding(
name,
Binding {
kind: BindingKind::FutureImportation,
used: Some(self.scopes.last().expect("No current scope found.").id),
location: stmt.location,
},
@@ -210,7 +211,6 @@ impl Visitor for Checker<'_> {
name,
Binding {
kind: BindingKind::StarImportation,
used: None,
location: stmt.location,
},
@@ -332,23 +332,23 @@ impl Visitor for Checker<'_> {
fn visit_annotation(&mut self, expr: &Expr) {
let initial = self.in_annotation;
self.in_annotation = true;
self.visit_expr(expr);
self.visit_expr(expr, None);
self.in_annotation = initial;
}
fn visit_expr(&mut self, expr: &Expr) {
fn visit_expr(&mut self, expr: &Expr, parent: Option<&Stmt>) {
let initial = self.in_f_string;
match &expr.node {
ExprKind::Name { ctx, .. } => match ctx {
ExprContext::Load => self.handle_node_load(expr),
ExprContext::Store => self.handle_node_store(expr),
ExprContext::Store => self.handle_node_store(expr, parent),
ExprContext::Del => self.handle_node_delete(expr),
},
ExprKind::GeneratorExp { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. } => self.push_scope(Scope::new(Generator)),
ExprKind::Lambda { .. } => self.push_scope(Scope::new(Function)),
| ExprKind::SetComp { .. } => self.push_scope(Scope::new(ScopeKind::Generator)),
ExprKind::Lambda { .. } => self.push_scope(Scope::new(ScopeKind::Function)),
ExprKind::Yield { .. } | ExprKind::YieldFrom { .. } => {
let scope = self.scopes.last().expect("No current scope found.");
if self
@@ -413,24 +413,30 @@ impl Visitor for Checker<'_> {
Some(name) => {
let scope = self.scopes.last().expect("No current scope found.");
if scope.values.contains_key(name) {
self.handle_node_store(&Expr::new(
self.handle_node_store(
&Expr::new(
excepthandler.location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
},
),
None,
);
}
let scope = self.scopes.last().expect("No current scope found.");
let prev_definition = scope.values.get(name).cloned();
self.handle_node_store(
&Expr::new(
excepthandler.location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
},
));
}
let scope = self.scopes.last().expect("No current scope found.");
let prev_definition = scope.values.get(name).cloned();
self.handle_node_store(&Expr::new(
excepthandler.location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
},
));
),
None,
);
walk_excepthandler(self, excepthandler);
@@ -559,7 +565,7 @@ impl Checker<'_> {
let mut first_iter = true;
let mut in_generators = false;
for scope in self.scopes.iter_mut().rev() {
if matches!(scope.kind, Class) {
if matches!(scope.kind, ScopeKind::Class) {
if id == "__class__" {
return;
} else if !first_iter && !in_generators {
@@ -572,7 +578,7 @@ impl Checker<'_> {
}
first_iter = false;
in_generators = matches!(scope.kind, Generator);
in_generators = matches!(scope.kind, ScopeKind::Generator);
}
if self.settings.select.contains(&CheckCode::F821) {
@@ -584,26 +590,29 @@ impl Checker<'_> {
}
}
fn handle_node_store(&mut self, expr: &Expr) {
fn handle_node_store(&mut self, expr: &Expr, parent: Option<&Stmt>) {
if let ExprKind::Name { id, .. } = &expr.node {
if self.settings.select.contains(&CheckCode::F832) {
let current = self.scopes.last().expect("No current scope found.");
if matches!(current.kind, ScopeKind::Function) && !current.values.contains_key(id) {
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function) || matches!(scope.kind, Module)
{
let used = scope
.values
.get(id)
.map(|binding| binding.used)
.unwrap_or_default();
if let Some(scope_id) = used {
if scope_id == current.id {
self.checks.push(Check {
kind: CheckKind::UndefinedLocal(id.clone()),
location: expr.location,
});
}
let current = self.scopes.last().expect("No current scope found.");
if self.settings.select.contains(&CheckCode::F823)
&& matches!(current.kind, ScopeKind::Function)
&& !current.values.contains_key(id)
{
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function)
|| matches!(scope.kind, ScopeKind::Module)
{
let used = scope
.values
.get(id)
.map(|binding| binding.used)
.unwrap_or_default();
if let Some(scope_id) = used {
if scope_id == current.id {
self.checks.push(Check {
kind: CheckKind::UndefinedLocal(id.clone()),
location: expr.location,
});
}
}
}
@@ -611,14 +620,36 @@ impl Checker<'_> {
}
// TODO(charlie): Handle alternate binding types (like `Annotation`).
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Assignment,
used: None,
location: expr.location,
},
);
if id == "__all__"
&& matches!(current.kind, ScopeKind::Module)
&& match parent {
None => false,
Some(stmt) => {
matches!(stmt.node, StmtKind::Assign { .. })
|| matches!(stmt.node, StmtKind::AugAssign { .. })
|| matches!(stmt.node, StmtKind::AnnAssign { .. })
}
}
{
// Really need parent here.
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Export(extract_all_names(parent.unwrap(), current)),
used: None,
location: expr.location,
},
);
} else {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Assignment,
used: None,
location: expr.location,
},
);
}
}
}
@@ -639,17 +670,50 @@ impl Checker<'_> {
fn check_deferred(&mut self, path: &str) {
for value in self.deferred.clone() {
if let Ok(expr) = &parser::parse_expression(&value, path) {
self.visit_expr(expr);
self.visit_expr(expr, None);
}
}
}
fn check_dead_scopes(&mut self) {
if self.settings.select.contains(&CheckCode::F401) {
// TODO(charlie): Handle `__all__`.
for scope in &self.dead_scopes {
for (_, binding) in scope.values.iter().rev() {
if binding.used.is_none() {
if !self.settings.select.contains(&CheckCode::F822)
&& !self.settings.select.contains(&CheckCode::F401)
{
return;
}
for scope in &self.dead_scopes {
let all_binding = scope.values.get("__all__");
let all_names = all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some(names),
_ => None,
});
if self.settings.select.contains(&CheckCode::F822)
&& !Path::new(self.path).ends_with("__init__.py")
{
if let Some(binding) = all_binding {
if let Some(names) = all_names {
for name in names {
if !scope.values.contains_key(name) {
self.checks.push(Check {
kind: CheckKind::UndefinedExport(name.to_string()),
location: binding.location,
});
}
}
}
}
}
if self.settings.select.contains(&CheckCode::F401) {
for (name, binding) in scope.values.iter().rev() {
let used = binding.used.is_some()
|| all_names
.map(|names| names.contains(name))
.unwrap_or_default();
if !used {
match &binding.kind {
BindingKind::Importation(full_name)
| BindingKind::SubmoduleImportation(full_name) => {
@@ -668,8 +732,8 @@ impl Checker<'_> {
}
pub fn check_ast(python_ast: &Suite, settings: &Settings, path: &str) -> Vec<Check> {
let mut checker = Checker::new(settings);
checker.push_scope(Scope::new(Module));
let mut checker = Checker::new(settings, path);
checker.push_scope(Scope::new(ScopeKind::Module));
checker.bind_builtins();
for stmt in python_ast {

View File

@@ -1,6 +1,7 @@
use std::str::FromStr;
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
@@ -15,10 +16,12 @@ pub enum CheckCode {
F704,
F706,
F821,
F822,
F823,
F831,
F832,
F841,
F901,
R0205,
}
impl FromStr for CheckCode {
@@ -34,10 +37,12 @@ impl FromStr for CheckCode {
"F704" => Ok(CheckCode::F704),
"F706" => Ok(CheckCode::F706),
"F821" => Ok(CheckCode::F821),
"F822" => Ok(CheckCode::F822),
"F823" => Ok(CheckCode::F823),
"F831" => Ok(CheckCode::F831),
"F832" => Ok(CheckCode::F832),
"F841" => Ok(CheckCode::F841),
"F901" => Ok(CheckCode::F901),
"R0205" => Ok(CheckCode::R0205),
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
}
}
@@ -54,10 +59,12 @@ impl CheckCode {
CheckCode::F704 => "F704",
CheckCode::F706 => "F706",
CheckCode::F821 => "F821",
CheckCode::F823 => "F823",
CheckCode::F822 => "F822",
CheckCode::F831 => "F831",
CheckCode::F832 => "F832",
CheckCode::F841 => "F841",
CheckCode::F901 => "F901",
CheckCode::R0205 => "R0205",
}
}
@@ -72,10 +79,12 @@ impl CheckCode {
CheckCode::F704 => &LintSource::AST,
CheckCode::F706 => &LintSource::AST,
CheckCode::F821 => &LintSource::AST,
CheckCode::F822 => &LintSource::AST,
CheckCode::F823 => &LintSource::AST,
CheckCode::F831 => &LintSource::AST,
CheckCode::F832 => &LintSource::AST,
CheckCode::F841 => &LintSource::AST,
CheckCode::F901 => &LintSource::AST,
CheckCode::R0205 => &LintSource::AST,
}
}
}
@@ -94,15 +103,37 @@ pub enum CheckKind {
ImportStarUsage,
LineTooLong,
RaiseNotImplemented,
YieldOutsideFunction,
ReturnOutsideFunction,
UndefinedName(String),
UndefinedLocal(String),
UnusedVariable(String),
UndefinedExport(String),
UndefinedName(String),
UnusedImport(String),
UnusedVariable(String),
UselessObjectInheritance(String),
YieldOutsideFunction,
}
impl CheckKind {
/// The name of the check.
pub fn name(&self) -> &'static str {
match self {
CheckKind::DuplicateArgumentName => "DuplicateArgumentName",
CheckKind::FStringMissingPlaceholders => "FStringMissingPlaceholders",
CheckKind::IfTuple => "IfTuple",
CheckKind::ImportStarUsage => "ImportStarUsage",
CheckKind::LineTooLong => "LineTooLong",
CheckKind::RaiseNotImplemented => "RaiseNotImplemented",
CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction",
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
CheckKind::UndefinedExport(_) => "UndefinedExport",
CheckKind::UndefinedName(_) => "UndefinedName",
CheckKind::UnusedImport(_) => "UnusedImport",
CheckKind::UnusedVariable(_) => "UnusedVariable",
CheckKind::UselessObjectInheritance(_) => "UselessObjectInheritance",
CheckKind::YieldOutsideFunction => "YieldOutsideFunction",
}
}
/// A four-letter shorthand code for the check.
pub fn code(&self) -> &'static CheckCode {
match self {
@@ -112,12 +143,14 @@ impl CheckKind {
CheckKind::ImportStarUsage => &CheckCode::F403,
CheckKind::LineTooLong => &CheckCode::E501,
CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::UndefinedExport(_) => &CheckCode::F822,
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UndefinedLocal(_) => &CheckCode::F832,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::UselessObjectInheritance(_) => &CheckCode::R0205,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
}
}
@@ -130,30 +163,34 @@ impl CheckKind {
CheckKind::FStringMissingPlaceholders => {
"f-string without any placeholders".to_string()
}
CheckKind::IfTuple => {
"If test is a tuple.to_string(), which is always `True`".to_string()
}
CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(),
CheckKind::ImportStarUsage => "Unable to detect undefined names".to_string(),
CheckKind::LineTooLong => "Line too long".to_string(),
CheckKind::RaiseNotImplemented => {
"'raise NotImplemented' should be 'raise NotImplementedError".to_string()
}
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
}
CheckKind::ReturnOutsideFunction => {
"a `return` statement outside of a function/method".to_string()
}
CheckKind::UndefinedExport(name) => {
format!("Undefined name `{name}` in __all__")
}
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
}
CheckKind::UndefinedLocal(name) => {
format!("Local variable `{name}` referenced before assignment")
}
CheckKind::UnusedImport(name) => format!("`{name}` imported but unused"),
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
}
CheckKind::UselessObjectInheritance(name) => {
format!("Class {name} inherits from object")
}
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
}
}
}
}
@@ -164,14 +201,17 @@ pub struct Check {
pub location: Location,
}
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?").expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
impl Check {
pub fn is_inline_ignored(&self, line: &str) -> bool {
let re = Regex::new(r"(?i)# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?").unwrap();
match re.captures(line) {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("codes") {
Some(codes) => {
let re = Regex::new(r"[,\s]").unwrap();
for code in re
for code in SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())

View File

@@ -1,3 +1,4 @@
mod ast_ops;
mod builtins;
mod cache;
pub mod check_ast;

View File

@@ -68,7 +68,7 @@ mod tests {
#[test]
fn e501() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/E501.py"),
Path::new("./resources/test/fixtures/E501.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -79,7 +79,7 @@ mod tests {
let expected = vec![Message {
kind: CheckKind::LineTooLong,
location: Location::new(5, 89),
filename: "./resources/test/src/E501.py".to_string(),
filename: "./resources/test/fixtures/E501.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
@@ -92,7 +92,7 @@ mod tests {
#[test]
fn f401() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F401.py"),
Path::new("./resources/test/fixtures/F401.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -103,18 +103,18 @@ mod tests {
let expected = vec![
Message {
kind: CheckKind::UnusedImport("logging.handlers".to_string()),
location: Location::new(11, 1),
filename: "./resources/test/src/F401.py".to_string(),
location: Location::new(12, 1),
filename: "./resources/test/fixtures/F401.py".to_string(),
},
Message {
kind: CheckKind::UnusedImport("functools".to_string()),
location: Location::new(2, 1),
filename: "./resources/test/src/F401.py".to_string(),
location: Location::new(3, 1),
filename: "./resources/test/fixtures/F401.py".to_string(),
},
Message {
kind: CheckKind::UnusedImport("collections.OrderedDict".to_string()),
location: Location::new(3, 1),
filename: "./resources/test/src/F401.py".to_string(),
location: Location::new(4, 1),
filename: "./resources/test/fixtures/F401.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -128,7 +128,7 @@ mod tests {
#[test]
fn f403() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F403.py"),
Path::new("./resources/test/fixtures/F403.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -140,12 +140,12 @@ mod tests {
Message {
kind: CheckKind::ImportStarUsage,
location: Location::new(1, 1),
filename: "./resources/test/src/F403.py".to_string(),
filename: "./resources/test/fixtures/F403.py".to_string(),
},
Message {
kind: CheckKind::ImportStarUsage,
location: Location::new(2, 1),
filename: "./resources/test/src/F403.py".to_string(),
filename: "./resources/test/fixtures/F403.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -158,7 +158,7 @@ mod tests {
#[test]
fn f541() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F541.py"),
Path::new("./resources/test/fixtures/F541.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -170,17 +170,17 @@ mod tests {
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(4, 7),
filename: "./resources/test/src/F541.py".to_string(),
filename: "./resources/test/fixtures/F541.py".to_string(),
},
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(5, 7),
filename: "./resources/test/src/F541.py".to_string(),
filename: "./resources/test/fixtures/F541.py".to_string(),
},
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(7, 7),
filename: "./resources/test/src/F541.py".to_string(),
filename: "./resources/test/fixtures/F541.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -194,7 +194,7 @@ mod tests {
#[test]
fn f634() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F634.py"),
Path::new("./resources/test/fixtures/F634.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -206,12 +206,12 @@ mod tests {
Message {
kind: CheckKind::IfTuple,
location: Location::new(1, 1),
filename: "./resources/test/src/F634.py".to_string(),
filename: "./resources/test/fixtures/F634.py".to_string(),
},
Message {
kind: CheckKind::IfTuple,
location: Location::new(7, 5),
filename: "./resources/test/src/F634.py".to_string(),
filename: "./resources/test/fixtures/F634.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -225,7 +225,7 @@ mod tests {
#[test]
fn f704() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F704.py"),
Path::new("./resources/test/fixtures/F704.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -237,17 +237,17 @@ mod tests {
Message {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(6, 5),
filename: "./resources/test/src/F704.py".to_string(),
filename: "./resources/test/fixtures/F704.py".to_string(),
},
Message {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(9, 1),
filename: "./resources/test/src/F704.py".to_string(),
filename: "./resources/test/fixtures/F704.py".to_string(),
},
Message {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(10, 1),
filename: "./resources/test/src/F704.py".to_string(),
filename: "./resources/test/fixtures/F704.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -261,7 +261,7 @@ mod tests {
#[test]
fn f706() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F706.py"),
Path::new("./resources/test/fixtures/F706.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -273,12 +273,12 @@ mod tests {
Message {
kind: CheckKind::ReturnOutsideFunction,
location: Location::new(6, 5),
filename: "./resources/test/src/F706.py".to_string(),
filename: "./resources/test/fixtures/F706.py".to_string(),
},
Message {
kind: CheckKind::ReturnOutsideFunction,
location: Location::new(9, 1),
filename: "./resources/test/src/F706.py".to_string(),
filename: "./resources/test/fixtures/F706.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -292,7 +292,7 @@ mod tests {
#[test]
fn f821() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F821.py"),
Path::new("./resources/test/fixtures/F821.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -304,22 +304,22 @@ mod tests {
Message {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(2, 12),
filename: "./resources/test/src/F821.py".to_string(),
filename: "./resources/test/fixtures/F821.py".to_string(),
},
Message {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(6, 13),
filename: "./resources/test/src/F821.py".to_string(),
filename: "./resources/test/fixtures/F821.py".to_string(),
},
Message {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(10, 9),
filename: "./resources/test/src/F821.py".to_string(),
filename: "./resources/test/fixtures/F821.py".to_string(),
},
Message {
kind: CheckKind::UndefinedName("numeric_string".to_string()),
location: Location::new(21, 12),
filename: "./resources/test/src/F821.py".to_string(),
filename: "./resources/test/fixtures/F821.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -330,10 +330,58 @@ mod tests {
Ok(())
}
#[test]
fn f822() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/F822.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F822]),
},
&cache::Mode::None,
)?;
let expected = vec![Message {
kind: CheckKind::UndefinedExport("b".to_string()),
location: Location::new(3, 1),
filename: "./resources/test/fixtures/F822.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f823() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/F823.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F823]),
},
&cache::Mode::None,
)?;
let expected = vec![Message {
kind: CheckKind::UndefinedLocal("my_var".to_string()),
location: Location::new(6, 5),
filename: "./resources/test/fixtures/F823.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f831() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F831.py"),
Path::new("./resources/test/fixtures/F831.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -345,17 +393,17 @@ mod tests {
Message {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(1, 25),
filename: "./resources/test/src/F831.py".to_string(),
filename: "./resources/test/fixtures/F831.py".to_string(),
},
Message {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(5, 28),
filename: "./resources/test/src/F831.py".to_string(),
filename: "./resources/test/fixtures/F831.py".to_string(),
},
Message {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(9, 27),
filename: "./resources/test/src/F831.py".to_string(),
filename: "./resources/test/fixtures/F831.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -366,34 +414,10 @@ mod tests {
Ok(())
}
#[test]
fn f832() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F832.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F832]),
},
&cache::Mode::None,
)?;
let expected = vec![Message {
kind: CheckKind::UndefinedLocal("my_var".to_string()),
location: Location::new(6, 5),
filename: "./resources/test/src/F832.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f841() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F841.py"),
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -405,12 +429,12 @@ mod tests {
Message {
kind: CheckKind::UnusedVariable("e".to_string()),
location: Location::new(3, 1),
filename: "./resources/test/src/F841.py".to_string(),
filename: "./resources/test/fixtures/F841.py".to_string(),
},
Message {
kind: CheckKind::UnusedVariable("z".to_string()),
location: Location::new(16, 5),
filename: "./resources/test/src/F841.py".to_string(),
filename: "./resources/test/fixtures/F841.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
@@ -424,7 +448,7 @@ mod tests {
#[test]
fn f901() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F901.py"),
Path::new("./resources/test/fixtures/F901.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
@@ -436,12 +460,48 @@ mod tests {
Message {
kind: CheckKind::RaiseNotImplemented,
location: Location::new(2, 5),
filename: "./resources/test/src/F901.py".to_string(),
filename: "./resources/test/fixtures/F901.py".to_string(),
},
Message {
kind: CheckKind::RaiseNotImplemented,
location: Location::new(6, 5),
filename: "./resources/test/src/F901.py".to_string(),
filename: "./resources/test/fixtures/F901.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn r0205() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/R0205.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::R0205]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::UselessObjectInheritance("B".to_string()),
location: Location::new(5, 1),
filename: "./resources/test/fixtures/R0205.py".to_string(),
},
Message {
kind: CheckKind::UselessObjectInheritance("C".to_string()),
location: Location::new(9, 1),
filename: "./resources/test/fixtures/R0205.py".to_string(),
},
Message {
kind: CheckKind::UselessObjectInheritance("D".to_string()),
location: Location::new(14, 5),
filename: "./resources/test/fixtures/R0205.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());

View File

@@ -14,12 +14,20 @@ pub fn load_config<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<(Pat
Some(project_root) => match find_pyproject_toml(&project_root) {
Some(path) => {
debug!("Found pyproject.toml at: {}", path.to_string_lossy());
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default();
Ok((project_root, config))
match parse_pyproject_toml(&path) {
Ok(pyproject) => {
let config = pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default();
Ok((project_root, config))
}
Err(e) => {
println!("Failed to load pyproject.toml: {:?}", e);
println!("Falling back to default configuration...");
Ok(Default::default())
}
}
}
None => Ok(Default::default()),
},
@@ -208,18 +216,17 @@ other-attribute = 1
#[test]
fn find_and_parse_pyproject_toml() -> Result<()> {
let project_root = find_project_root([Path::new("resources/test/src/__init__.py")])
let project_root = find_project_root([Path::new("resources/test/fixtures/__init__.py")])
.expect("Unable to find project root.");
assert_eq!(project_root, Path::new("resources/test/src"));
assert_eq!(project_root, Path::new("resources/test/fixtures"));
let path = find_pyproject_toml(&project_root).expect("Unable to find pyproject.toml.");
assert_eq!(path, Path::new("resources/test/src/pyproject.toml"));
assert_eq!(path, Path::new("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
.tool
.map(|tool| tool.ruff)
.flatten()
.and_then(|tool| tool.ruff)
.expect("Unable to find tool.ruff.");
assert_eq!(
config,
@@ -238,10 +245,12 @@ other-attribute = 1
CheckCode::F704,
CheckCode::F706,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F832,
CheckCode::F841,
CheckCode::F901,
CheckCode::R0205,
])),
}
);

View File

@@ -51,7 +51,7 @@ impl Settings {
CheckCode::F634,
CheckCode::F706,
CheckCode::F831,
CheckCode::F832,
CheckCode::F823,
CheckCode::F901,
])
}),

View File

@@ -11,7 +11,7 @@ pub trait Visitor {
fn visit_annotation(&mut self, expr: &Expr) {
walk_expr(self, expr);
}
fn visit_expr(&mut self, expr: &Expr) {
fn visit_expr(&mut self, expr: &Expr, _parent: Option<&Stmt>) {
walk_expr(self, expr);
}
fn visit_constant(&mut self, constant: &Constant) {
@@ -63,82 +63,43 @@ pub trait Visitor {
pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
match &stmt.node {
StmtKind::FunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
StmtKind::FunctionDef { args, body, .. } => {
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)
}
}
StmtKind::AsyncFunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
StmtKind::AsyncFunctionDef { args, body, .. } => {
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)
}
}
StmtKind::ClassDef {
bases,
keywords,
body,
decorator_list,
..
} => {
for expr in bases {
visitor.visit_expr(expr)
}
for keyword in keywords {
visitor.visit_keyword(keyword)
}
StmtKind::ClassDef { body, .. } => {
for stmt in body {
visitor.visit_stmt(stmt)
}
for expr in decorator_list {
visitor.visit_expr(expr)
}
}
StmtKind::Return { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Delete { targets } => {
for expr in targets {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Assign { targets, value, .. } => {
for expr in targets {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
visitor.visit_expr(value)
visitor.visit_expr(value, Some(stmt))
}
StmtKind::AugAssign { target, op, value } => {
visitor.visit_expr(target);
visitor.visit_expr(target, Some(stmt));
visitor.visit_operator(op);
visitor.visit_expr(value);
visitor.visit_expr(value, Some(stmt));
}
StmtKind::AnnAssign {
target,
@@ -146,10 +107,10 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
value,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(target, Some(stmt));
visitor.visit_annotation(annotation);
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::For {
@@ -159,8 +120,8 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(iter);
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -175,8 +136,8 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(iter);
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -185,7 +146,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::While { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(test, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -194,7 +155,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::If { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(test, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -220,17 +181,17 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
StmtKind::Match { subject, cases } => {
// TODO(charlie): Handle `cases`.
visitor.visit_expr(subject);
visitor.visit_expr(subject, Some(stmt));
for match_case in cases {
visitor.visit_match_case(match_case);
}
}
StmtKind::Raise { exc, cause } => {
if let Some(expr) = exc {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
};
if let Some(expr) = cause {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
};
}
StmtKind::Try {
@@ -253,9 +214,9 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::Assert { test, msg } => {
visitor.visit_expr(test);
visitor.visit_expr(test, None);
if let Some(expr) = msg {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Import { names } => {
@@ -270,7 +231,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
StmtKind::Global { .. } => {}
StmtKind::Nonlocal { .. } => {}
StmtKind::Expr { value } => visitor.visit_expr(value),
StmtKind::Expr { value } => visitor.visit_expr(value, Some(stmt)),
StmtKind::Pass => {}
StmtKind::Break => {}
StmtKind::Continue => {}
@@ -282,55 +243,55 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
ExprKind::BoolOp { op, values } => {
visitor.visit_boolop(op);
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::NamedExpr { target, value } => {
visitor.visit_expr(target);
visitor.visit_expr(value);
visitor.visit_expr(target, None);
visitor.visit_expr(value, None);
}
ExprKind::BinOp { left, op, right } => {
visitor.visit_expr(left);
visitor.visit_expr(left, None);
visitor.visit_operator(op);
visitor.visit_expr(right);
visitor.visit_expr(right, None);
}
ExprKind::UnaryOp { op, operand } => {
visitor.visit_unaryop(op);
visitor.visit_expr(operand);
visitor.visit_expr(operand, None);
}
ExprKind::Lambda { args, body } => {
visitor.visit_arguments(args);
visitor.visit_expr(body);
visitor.visit_expr(body, None);
}
ExprKind::IfExp { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(body);
visitor.visit_expr(orelse);
visitor.visit_expr(test, None);
visitor.visit_expr(body, None);
visitor.visit_expr(orelse, None);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Set { elts } => {
for expr in elts {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::ListComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt);
visitor.visit_expr(elt, None);
}
ExprKind::SetComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt);
visitor.visit_expr(elt, None);
}
ExprKind::DictComp {
key,
@@ -340,33 +301,33 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(key);
visitor.visit_expr(value);
visitor.visit_expr(key, None);
visitor.visit_expr(value, None);
}
ExprKind::GeneratorExp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt);
visitor.visit_expr(elt, None);
}
ExprKind::Await { value } => visitor.visit_expr(value),
ExprKind::Await { value } => visitor.visit_expr(value, None),
ExprKind::Yield { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::YieldFrom { value } => visitor.visit_expr(value),
ExprKind::YieldFrom { value } => visitor.visit_expr(value, None),
ExprKind::Compare {
left,
ops,
comparators,
} => {
visitor.visit_expr(left);
visitor.visit_expr(left, None);
for cmpop in ops {
visitor.visit_cmpop(cmpop);
}
for expr in comparators {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Call {
@@ -374,9 +335,9 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
args,
keywords,
} => {
visitor.visit_expr(func);
visitor.visit_expr(func, None);
for expr in args {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
@@ -385,28 +346,28 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
ExprKind::FormattedValue {
value, format_spec, ..
} => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
if let Some(expr) = format_spec {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::JoinedStr { values } => {
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Constant { value, .. } => visitor.visit_constant(value),
ExprKind::Attribute { value, ctx, .. } => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Subscript { value, slice, ctx } => {
visitor.visit_expr(value);
visitor.visit_expr(slice);
visitor.visit_expr(value, None);
visitor.visit_expr(slice, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Starred { value, ctx } => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Name { ctx, .. } => {
@@ -414,25 +375,25 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
}
ExprKind::List { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Tuple { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Slice { lower, upper, step } => {
if let Some(expr) = lower {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
if let Some(expr) = upper {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
if let Some(expr) = step {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
}
@@ -447,10 +408,10 @@ 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);
visitor.visit_expr(&comprehension.iter);
visitor.visit_expr(&comprehension.target, None);
visitor.visit_expr(&comprehension.iter, None);
for expr in &comprehension.ifs {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
@@ -458,7 +419,7 @@ pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &
match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { type_, body, .. } => {
if let Some(expr) = type_ {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for stmt in body {
visitor.visit_stmt(stmt);
@@ -481,13 +442,13 @@ pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Argument
visitor.visit_arg(arg);
}
for expr in &arguments.kw_defaults {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
if let Some(arg) = &arguments.kwarg {
visitor.visit_arg(arg)
}
for expr in &arguments.defaults {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
@@ -498,20 +459,20 @@ pub fn walk_arg<V: Visitor + ?Sized>(visitor: &mut V, arg: &Arg) {
}
pub fn walk_keyword<V: Visitor + ?Sized>(visitor: &mut V, keyword: &Keyword) {
visitor.visit_expr(&keyword.node.value);
visitor.visit_expr(&keyword.node.value, None);
}
pub fn walk_withitem<V: Visitor + ?Sized>(visitor: &mut V, withitem: &Withitem) {
visitor.visit_expr(&withitem.context_expr);
visitor.visit_expr(&withitem.context_expr, None);
if let Some(expr) = &withitem.optional_vars {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
pub fn walk_match_case<V: Visitor + ?Sized>(visitor: &mut V, match_case: &MatchCase) {
visitor.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for stmt in &match_case.body {
visitor.visit_stmt(stmt);
@@ -520,7 +481,7 @@ pub fn walk_match_case<V: Visitor + ?Sized>(visitor: &mut V, match_case: &MatchC
pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
match &pattern.node {
PatternKind::MatchValue { value } => visitor.visit_expr(value),
PatternKind::MatchValue { value } => visitor.visit_expr(value, None),
PatternKind::MatchSingleton { value } => visitor.visit_constant(value),
PatternKind::MatchSequence { patterns } => {
for pattern in patterns {
@@ -529,7 +490,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
}
PatternKind::MatchMapping { keys, patterns, .. } => {
for expr in keys {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for pattern in patterns {
visitor.visit_pattern(pattern);
@@ -541,7 +502,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
kwd_patterns,
..
} => {
visitor.visit_expr(cls);
visitor.visit_expr(cls, None);
for pattern in patterns {
visitor.visit_pattern(pattern);
}