Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e7878ff48 | ||
|
|
5113ded22a | ||
|
|
bf7bf7aa17 | ||
|
|
560c00ff9d | ||
|
|
befe64a10e | ||
|
|
4eccfdeb69 | ||
|
|
4123ba9851 | ||
|
|
e727c24f79 | ||
|
|
bd3b40688f | ||
|
|
b5549382a7 |
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.110
|
||||
rev: v0.0.112
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -933,7 +933,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.110-dev.0"
|
||||
version = "0.0.112-dev.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.22",
|
||||
@@ -1622,6 +1622,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.1.2"
|
||||
@@ -2234,11 +2240,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.110"
|
||||
version = "0.0.112"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"bincode",
|
||||
"bitflags",
|
||||
"cacache",
|
||||
"chrono",
|
||||
"clap 4.0.22",
|
||||
@@ -2255,6 +2262,7 @@ dependencies = [
|
||||
"itertools",
|
||||
"libcst",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"notify",
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
@@ -2279,7 +2287,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.110"
|
||||
version = "0.0.112"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.22",
|
||||
|
||||
@@ -6,7 +6,7 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.110"
|
||||
version = "0.0.112"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -15,6 +15,7 @@ name = "ruff"
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.66" }
|
||||
bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "1.3.2" }
|
||||
chrono = { version = "0.4.21" }
|
||||
clap = { version = "4.0.1", features = ["derive"] }
|
||||
colored = { version = "2.0.0" }
|
||||
@@ -26,6 +27,7 @@ glob = { version = "0.3.0" }
|
||||
itertools = { version = "0.10.5" }
|
||||
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" }
|
||||
log = { version = "0.4.17" }
|
||||
nohash-hasher = { version = "0.2.0" }
|
||||
notify = { version = "4.0.17" }
|
||||
num-bigint = { version = "0.4.3" }
|
||||
once_cell = { version = "1.16.0" }
|
||||
|
||||
16
README.md
16
README.md
@@ -98,7 +98,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.110
|
||||
rev: v0.0.112
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -501,7 +501,8 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
|
||||
| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults. | |
|
||||
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 |
|
||||
| B008 | FunctionCallArgumentDefault | Do not perform function calls in argument defaults. | |
|
||||
| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value, it is not any safer than normal property access. | |
|
||||
| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value, it is not any safer than normal property access. | 🛠 |
|
||||
| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value, it is not any safer than normal property access. | |
|
||||
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
|
||||
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | |
|
||||
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
|
||||
@@ -665,7 +666,7 @@ including:
|
||||
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
|
||||
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (19/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (20/32)
|
||||
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34)
|
||||
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
|
||||
|
||||
@@ -688,7 +689,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
|
||||
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
|
||||
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (19/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (20/32)
|
||||
|
||||
Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa),
|
||||
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34).
|
||||
@@ -713,8 +714,11 @@ project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
|
||||
|
||||
### How does Ruff's import sorting compare to [`isort`](https://pypi.org/project/isort/)?
|
||||
|
||||
Ruff's import sorting is intended to be equivalent to `isort` when used `profile = "black"` and
|
||||
`combine_as_imports = true`. Like `isort`, Ruff's import sorting is compatible with Black.
|
||||
Ruff's import sorting is intended to be equivalent to `isort` when used `profile = "black"`, and a
|
||||
few other settings (`combine_as_imports = true`, `order_by_type = false`, and
|
||||
`case_sensitive` = true`).
|
||||
|
||||
Like `isort`, Ruff's import sorting is compatible with Black.
|
||||
|
||||
Ruff is less configurable than `isort`, but supports the `known-first-party`, `known-third-party`,
|
||||
`extra-standard-library`, and `src` settings, like so:
|
||||
|
||||
4
flake8_to_ruff/Cargo.lock
generated
4
flake8_to_ruff/Cargo.lock
generated
@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8_to_ruff"
|
||||
version = "0.0.110"
|
||||
version = "0.0.112"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.110"
|
||||
version = "0.0.112"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.110-dev.0"
|
||||
version = "0.0.112-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
27
resources/test/fixtures/B009_B010.py
vendored
27
resources/test/fixtures/B009_B010.py
vendored
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Should emit:
|
||||
B009 - Line 17, 18, 19, 44
|
||||
B010 - Line 28, 29, 30
|
||||
B009 - Line 18, 19, 20, 21, 22
|
||||
B010 - Line 33, 34, 35, 36
|
||||
"""
|
||||
|
||||
# Valid getattr usage
|
||||
@@ -11,37 +11,26 @@ getattr(foo, "bar{foo}".format(foo="a"), None)
|
||||
getattr(foo, "bar{foo}".format(foo="a"))
|
||||
getattr(foo, bar, None)
|
||||
getattr(foo, "123abc")
|
||||
getattr(foo, r"123\abc")
|
||||
getattr(foo, "except")
|
||||
|
||||
# Invalid usage
|
||||
getattr(foo, "bar")
|
||||
getattr(foo, "_123abc")
|
||||
getattr(foo, "abc123")
|
||||
getattr(foo, r"abc123")
|
||||
_ = lambda x: getattr(x, "bar")
|
||||
|
||||
# Valid setattr usage
|
||||
setattr(foo, bar, None)
|
||||
setattr(foo, "bar{foo}".format(foo="a"), None)
|
||||
setattr(foo, "123abc", None)
|
||||
setattr(foo, r"123\abc", None)
|
||||
setattr(foo, "except", None)
|
||||
_ = lambda x: setattr(x, "bar", 1)
|
||||
|
||||
# Invalid usage
|
||||
setattr(foo, "bar", None)
|
||||
setattr(foo, "_123abc", None)
|
||||
setattr(foo, "abc123", None)
|
||||
|
||||
# Allow use of setattr within lambda expression
|
||||
# since assignment is not valid in this context.
|
||||
c = lambda x: setattr(x, "some_attr", 1)
|
||||
|
||||
|
||||
class FakeCookieStore:
|
||||
def __init__(self, has_setter):
|
||||
self.cookie_filter = None
|
||||
if has_setter:
|
||||
self.setCookieFilter = lambda func: setattr(self, "cookie_filter", func)
|
||||
|
||||
|
||||
# getattr is still flagged within lambda though
|
||||
c = lambda x: getattr(x, "some_attr")
|
||||
# should be replaced with
|
||||
c = lambda x: x.some_attr
|
||||
setattr(foo, r"abc123", None)
|
||||
|
||||
10
resources/test/fixtures/isort/skip.py
vendored
Normal file
10
resources/test/fixtures/isort/skip.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# isort: off
|
||||
import sys
|
||||
import os
|
||||
import collections
|
||||
# isort: on
|
||||
|
||||
import sys
|
||||
import os # isort: skip
|
||||
import collections
|
||||
import abc
|
||||
0
ruff/__init__.py
Normal file
0
ruff/__init__.py
Normal file
7
ruff/__main__.py
Normal file
7
ruff/__main__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
if __name__ == "__main__":
|
||||
ruff = os.path.join(sysconfig.get_path("scripts"), "ruff")
|
||||
os.spawnv(os.P_WAIT, ruff, [ruff, *sys.argv[1:]])
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.110"
|
||||
version = "0.0.112"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -46,6 +46,7 @@ pub enum ScopeKind<'a> {
|
||||
Generator,
|
||||
Module,
|
||||
Arg,
|
||||
Lambda,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -23,7 +23,6 @@ use crate::ast::{helpers, operations, visitor};
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
|
||||
use crate::isort::track::ImportTracker;
|
||||
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use crate::python::future::ALL_FEATURE_NAMES;
|
||||
use crate::python::typing;
|
||||
@@ -78,7 +77,6 @@ pub struct Checker<'a> {
|
||||
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>, VisibleScope)>,
|
||||
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
|
||||
deferred_assignments: Vec<usize>,
|
||||
import_tracker: ImportTracker<'a>,
|
||||
// Internal, derivative state.
|
||||
visible_scope: VisibleScope,
|
||||
in_f_string: Option<Range>,
|
||||
@@ -117,7 +115,6 @@ impl<'a> Checker<'a> {
|
||||
deferred_functions: Default::default(),
|
||||
deferred_lambdas: Default::default(),
|
||||
deferred_assignments: Default::default(),
|
||||
import_tracker: ImportTracker::new(),
|
||||
// Internal, derivative state.
|
||||
visible_scope: VisibleScope {
|
||||
modifier: Modifier::Module,
|
||||
@@ -185,9 +182,6 @@ where
|
||||
'b: 'a,
|
||||
{
|
||||
fn visit_stmt(&mut self, stmt: &'b Stmt) {
|
||||
// Call-through to any composed visitors.
|
||||
self.import_tracker.visit_stmt(stmt);
|
||||
|
||||
self.push_parent(stmt);
|
||||
|
||||
// Track whether we've seen docstrings, non-imports, etc.
|
||||
@@ -1074,6 +1068,16 @@ where
|
||||
if self.settings.enabled.contains(&CheckCode::B009) {
|
||||
flake8_bugbear::plugins::getattr_with_constant(self, expr, func, args);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::B010) {
|
||||
if !self
|
||||
.scope_stack
|
||||
.iter()
|
||||
.rev()
|
||||
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda))
|
||||
{
|
||||
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
|
||||
}
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::B026) {
|
||||
flake8_bugbear::plugins::star_arg_unpacking_after_keyword_arg(
|
||||
self, args, keywords,
|
||||
@@ -1461,8 +1465,8 @@ where
|
||||
for expr in &args.defaults {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.push_scope(Scope::new(ScopeKind::Lambda))
|
||||
}
|
||||
|
||||
ExprKind::ListComp { elt, generators } | ExprKind::SetComp { elt, generators } => {
|
||||
if self.settings.enabled.contains(&CheckCode::C416) {
|
||||
if let Some(check) = flake8_comprehensions::checks::unnecessary_comprehension(
|
||||
@@ -1478,7 +1482,6 @@ where
|
||||
}
|
||||
self.push_scope(Scope::new(ScopeKind::Generator))
|
||||
}
|
||||
|
||||
ExprKind::GeneratorExp { .. } | ExprKind::DictComp { .. } => {
|
||||
self.push_scope(Scope::new(ScopeKind::Generator))
|
||||
}
|
||||
@@ -1649,7 +1652,8 @@ where
|
||||
|
||||
// Post-visit.
|
||||
match &expr.node {
|
||||
ExprKind::GeneratorExp { .. }
|
||||
ExprKind::Lambda { .. }
|
||||
| ExprKind::GeneratorExp { .. }
|
||||
| ExprKind::ListComp { .. }
|
||||
| ExprKind::DictComp { .. }
|
||||
| ExprKind::SetComp { .. } => {
|
||||
@@ -1664,9 +1668,6 @@ where
|
||||
}
|
||||
|
||||
fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) {
|
||||
// Call-through to any composed visitors.
|
||||
self.import_tracker.visit_excepthandler(excepthandler);
|
||||
|
||||
match &excepthandler.node {
|
||||
ExcepthandlerKind::ExceptHandler { type_, name, .. } => {
|
||||
if self.settings.enabled.contains(&CheckCode::E722) && type_.is_none() {
|
||||
@@ -2244,7 +2245,7 @@ impl<'a> Checker<'a> {
|
||||
while let Some((expr, scopes, parents)) = self.deferred_lambdas.pop() {
|
||||
self.parent_stack = parents;
|
||||
self.scope_stack = scopes;
|
||||
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
|
||||
self.push_scope(Scope::new(ScopeKind::Lambda));
|
||||
|
||||
if let ExprKind::Lambda { args, body } = &expr.node {
|
||||
self.visit_arguments(args);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Lint rules based on import analysis.
|
||||
|
||||
use nohash_hasher::IntSet;
|
||||
use rustpython_parser::ast::Suite;
|
||||
|
||||
use crate::ast::visitor::Visitor;
|
||||
@@ -30,10 +31,11 @@ fn check_import_blocks(
|
||||
pub fn check_imports(
|
||||
python_ast: &Suite,
|
||||
locator: &SourceCodeLocator,
|
||||
exclusions: &IntSet<usize>,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Vec<Check> {
|
||||
let mut tracker = ImportTracker::new();
|
||||
let mut tracker = ImportTracker::new(exclusions);
|
||||
for stmt in python_ast {
|
||||
tracker.visit_stmt(stmt);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Lint rules based on checking raw physical lines.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use nohash_hasher::IntMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_parser::ast::Location;
|
||||
@@ -36,7 +35,7 @@ fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
|
||||
pub fn check_lines(
|
||||
checks: &mut Vec<Check>,
|
||||
contents: &str,
|
||||
noqa_line_for: &[usize],
|
||||
noqa_line_for: &IntMap<usize, usize>,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) {
|
||||
@@ -44,18 +43,23 @@ pub fn check_lines(
|
||||
let enforce_line_too_long = settings.enabled.contains(&CheckCode::E501);
|
||||
let enforce_noqa = settings.enabled.contains(&CheckCode::M001);
|
||||
|
||||
let mut noqa_directives: BTreeMap<usize, (Directive, Vec<&str>)> = BTreeMap::new();
|
||||
|
||||
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
|
||||
let mut line_checks = vec![];
|
||||
let mut ignored = vec![];
|
||||
|
||||
checks.sort_by_key(|check| check.location);
|
||||
let mut checks_iter = checks.iter().enumerate().peekable();
|
||||
if let Some((_index, check)) = checks_iter.peek() {
|
||||
assert!(check.location.row() >= 1);
|
||||
}
|
||||
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
for (lineno, line) in lines.iter().enumerate() {
|
||||
// Grab the noqa (logical) line number for the current (physical) line.
|
||||
// If there are newlines at the end of the file, they won't be represented in
|
||||
// `noqa_line_for`, so fallback to the current line.
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(lineno)
|
||||
.get(&lineno)
|
||||
.map(|lineno| lineno - 1)
|
||||
.unwrap_or(lineno);
|
||||
|
||||
@@ -90,26 +94,25 @@ pub fn check_lines(
|
||||
}
|
||||
|
||||
// Remove any ignored checks.
|
||||
// TODO(charlie): Only validate checks for the current line.
|
||||
for (index, check) in checks.iter().enumerate() {
|
||||
if check.location.row() == lineno + 1 {
|
||||
let noqa = noqa_directives
|
||||
.entry(noqa_lineno)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
||||
while let Some((index, check)) =
|
||||
checks_iter.next_if(|(_index, check)| check.location.row() == lineno + 1)
|
||||
{
|
||||
let noqa = noqa_directives
|
||||
.entry(noqa_lineno)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
||||
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
ignored.push(index)
|
||||
}
|
||||
(Directive::Codes(_, _, codes), matches) => {
|
||||
if codes.contains(&check.kind.code().as_ref()) {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
ignored.push(index);
|
||||
}
|
||||
}
|
||||
(Directive::None, _) => {}
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
ignored.push(index)
|
||||
}
|
||||
(Directive::Codes(_, _, codes), matches) => {
|
||||
if codes.contains(&check.kind.code().as_ref()) {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
ignored.push(index);
|
||||
}
|
||||
}
|
||||
(Directive::None, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +156,7 @@ pub fn check_lines(
|
||||
if let Some(line) = lines.last() {
|
||||
let lineno = lines.len() - 1;
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(lineno)
|
||||
.get(&lineno)
|
||||
.map(|lineno| lineno - 1)
|
||||
.unwrap_or(lineno);
|
||||
|
||||
@@ -257,6 +260,8 @@ pub fn check_lines(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nohash_hasher::IntMap;
|
||||
|
||||
use super::check_lines;
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
@@ -265,7 +270,7 @@ mod tests {
|
||||
#[test]
|
||||
fn e501_non_ascii_char() {
|
||||
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
|
||||
let noqa_line_for: Vec<usize> = vec![1];
|
||||
let noqa_line_for: IntMap<usize, usize> = Default::default();
|
||||
let check_with_max_line_length = |line_length: usize| {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
check_lines(
|
||||
|
||||
@@ -85,6 +85,7 @@ pub enum CheckCode {
|
||||
B007,
|
||||
B008,
|
||||
B009,
|
||||
B010,
|
||||
B011,
|
||||
B013,
|
||||
B014,
|
||||
@@ -357,6 +358,7 @@ pub enum CheckKind {
|
||||
UnusedLoopControlVariable(String),
|
||||
FunctionCallArgumentDefault,
|
||||
GetAttrWithConstant,
|
||||
SetAttrWithConstant,
|
||||
DoNotAssertFalse,
|
||||
RedundantTupleInExceptionHandler(String),
|
||||
DuplicateHandlerException(Vec<String>),
|
||||
@@ -573,6 +575,7 @@ impl CheckCode {
|
||||
CheckCode::B007 => CheckKind::UnusedLoopControlVariable("i".to_string()),
|
||||
CheckCode::B008 => CheckKind::FunctionCallArgumentDefault,
|
||||
CheckCode::B009 => CheckKind::GetAttrWithConstant,
|
||||
CheckCode::B010 => CheckKind::SetAttrWithConstant,
|
||||
CheckCode::B011 => CheckKind::DoNotAssertFalse,
|
||||
CheckCode::B013 => {
|
||||
CheckKind::RedundantTupleInExceptionHandler("ValueError".to_string())
|
||||
@@ -794,6 +797,7 @@ impl CheckCode {
|
||||
CheckCode::B007 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B008 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B009 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B010 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B011 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B013 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B014 => CheckCategory::Flake8Bugbear,
|
||||
@@ -978,6 +982,7 @@ impl CheckKind {
|
||||
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
|
||||
CheckKind::FunctionCallArgumentDefault => &CheckCode::B008,
|
||||
CheckKind::GetAttrWithConstant => &CheckCode::B009,
|
||||
CheckKind::SetAttrWithConstant => &CheckCode::B010,
|
||||
CheckKind::DoNotAssertFalse => &CheckCode::B011,
|
||||
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
|
||||
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
|
||||
@@ -1287,6 +1292,10 @@ impl CheckKind {
|
||||
value, it is not any safer than normal property \
|
||||
access."
|
||||
.to_string(),
|
||||
CheckKind::SetAttrWithConstant => "Do not call `setattr` with a constant attribute \
|
||||
value, it is not any safer than normal property \
|
||||
access."
|
||||
.to_string(),
|
||||
CheckKind::DoNotAssertFalse => "Do not `assert False` (`python -O` removes these \
|
||||
calls), raise `AssertionError()`"
|
||||
.to_string(),
|
||||
@@ -1734,6 +1743,7 @@ impl CheckKind {
|
||||
| CheckKind::DeprecatedUnittestAlias(_, _)
|
||||
| CheckKind::DoNotAssertFalse
|
||||
| CheckKind::DuplicateHandlerException(_)
|
||||
| CheckKind::GetAttrWithConstant
|
||||
| CheckKind::IsLiteral
|
||||
| CheckKind::NewLineAfterLastParagraph
|
||||
| CheckKind::NewLineAfterSectionName(_)
|
||||
|
||||
@@ -45,6 +45,7 @@ pub enum CheckCodePrefix {
|
||||
B008,
|
||||
B009,
|
||||
B01,
|
||||
B010,
|
||||
B011,
|
||||
B013,
|
||||
B014,
|
||||
@@ -342,6 +343,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B007,
|
||||
CheckCode::B008,
|
||||
CheckCode::B009,
|
||||
CheckCode::B010,
|
||||
CheckCode::B011,
|
||||
CheckCode::B013,
|
||||
CheckCode::B014,
|
||||
@@ -361,6 +363,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B007,
|
||||
CheckCode::B008,
|
||||
CheckCode::B009,
|
||||
CheckCode::B010,
|
||||
CheckCode::B011,
|
||||
CheckCode::B013,
|
||||
CheckCode::B014,
|
||||
@@ -390,6 +393,7 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B008 => vec![CheckCode::B008],
|
||||
CheckCodePrefix::B009 => vec![CheckCode::B009],
|
||||
CheckCodePrefix::B01 => vec![
|
||||
CheckCode::B010,
|
||||
CheckCode::B011,
|
||||
CheckCode::B013,
|
||||
CheckCode::B014,
|
||||
@@ -398,6 +402,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B017,
|
||||
CheckCode::B018,
|
||||
],
|
||||
CheckCodePrefix::B010 => vec![CheckCode::B010],
|
||||
CheckCodePrefix::B011 => vec![CheckCode::B011],
|
||||
CheckCodePrefix::B013 => vec![CheckCode::B013],
|
||||
CheckCodePrefix::B014 => vec![CheckCode::B014],
|
||||
@@ -1066,6 +1071,7 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B008 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B009 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B01 => PrefixSpecificity::Tens,
|
||||
CheckCodePrefix::B010 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B011 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B013 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B014 => PrefixSpecificity::Explicit,
|
||||
|
||||
206
src/directives.rs
Normal file
206
src/directives.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
//! Extract `# noqa` and `# isort: skip` directives from tokenized source.
|
||||
|
||||
use bitflags::bitflags;
|
||||
use nohash_hasher::{IntMap, IntSet};
|
||||
use rustpython_ast::Location;
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::LintSource;
|
||||
use crate::{Settings, SourceCodeLocator};
|
||||
|
||||
bitflags! {
|
||||
pub struct Flags: u32 {
|
||||
const NOQA = 0b00000001;
|
||||
const ISORT = 0b00000010;
|
||||
}
|
||||
}
|
||||
|
||||
impl Flags {
|
||||
pub fn from_settings(settings: &Settings) -> Self {
|
||||
if settings
|
||||
.enabled
|
||||
.iter()
|
||||
.any(|check_code| matches!(check_code.lint_source(), LintSource::Imports))
|
||||
{
|
||||
Flags::NOQA | Flags::ISORT
|
||||
} else {
|
||||
Flags::NOQA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Directives {
|
||||
pub noqa_line_for: IntMap<usize, usize>,
|
||||
pub isort_exclusions: IntSet<usize>,
|
||||
}
|
||||
|
||||
pub fn extract_directives(
|
||||
lxr: &[LexResult],
|
||||
locator: &SourceCodeLocator,
|
||||
flags: &Flags,
|
||||
) -> Directives {
|
||||
Directives {
|
||||
noqa_line_for: if flags.contains(Flags::NOQA) {
|
||||
extract_noqa_line_for(lxr)
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
isort_exclusions: if flags.contains(Flags::ISORT) {
|
||||
extract_isort_exclusions(lxr, locator)
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a mapping from logical line to noqa line.
|
||||
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
|
||||
let mut noqa_line_for: IntMap<usize, usize> = IntMap::default();
|
||||
for (start, tok, end) in lxr.iter().flatten() {
|
||||
if matches!(tok, Tok::EndOfFile) {
|
||||
break;
|
||||
}
|
||||
// For multi-line strings, we expect `noqa` directives on the last line of the
|
||||
// string.
|
||||
if matches!(tok, Tok::String { .. }) && end.row() > start.row() {
|
||||
for i in start.row()..end.row() {
|
||||
noqa_line_for.insert(i, end.row());
|
||||
}
|
||||
}
|
||||
}
|
||||
noqa_line_for
|
||||
}
|
||||
|
||||
/// Extract a set of lines over which to disable isort.
|
||||
pub fn extract_isort_exclusions(lxr: &[LexResult], locator: &SourceCodeLocator) -> IntSet<usize> {
|
||||
let mut exclusions: IntSet<usize> = IntSet::default();
|
||||
let mut off: Option<&Location> = None;
|
||||
for (start, tok, end) in lxr.iter().flatten() {
|
||||
// TODO(charlie): Modify RustPython to include the comment text in the token.
|
||||
if matches!(tok, Tok::Comment) {
|
||||
let comment_text = locator.slice_source_code_range(&Range {
|
||||
location: *start,
|
||||
end_location: *end,
|
||||
});
|
||||
if off.is_some() {
|
||||
if comment_text == "# isort: on" {
|
||||
if let Some(start) = off {
|
||||
for row in start.row() + 1..=end.row() {
|
||||
exclusions.insert(row);
|
||||
}
|
||||
}
|
||||
off = None;
|
||||
}
|
||||
} else {
|
||||
if comment_text.contains("isort: skip") || comment_text.contains("isort:skip") {
|
||||
exclusions.insert(start.row());
|
||||
} else if comment_text == "# isort: off" {
|
||||
off = Some(start);
|
||||
}
|
||||
}
|
||||
} else if matches!(tok, Tok::EndOfFile) {
|
||||
if let Some(start) = off {
|
||||
for row in start.row() + 1..=end.row() {
|
||||
exclusions.insert(row);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
exclusions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use nohash_hasher::IntMap;
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
|
||||
use crate::directives::extract_noqa_line_for;
|
||||
|
||||
#[test]
|
||||
fn extraction() -> Result<()> {
|
||||
let empty: IntMap<usize, usize> = Default::default();
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"
|
||||
x = 1
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = 2
|
||||
z = x + 1
|
||||
",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
|
||||
y = 2
|
||||
z = x + 1
|
||||
",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = '''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(1, 4), (2, 4), (3, 4)])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = '''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
z = 2",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = '''abc
|
||||
def
|
||||
ghi
|
||||
'''",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
src/flake8_bugbear/constants.rs
Normal file
5
src/flake8_bugbear/constants.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
pub static IDENTIFIER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap());
|
||||
@@ -1 +1,2 @@
|
||||
mod constants;
|
||||
pub mod plugins;
|
||||
|
||||
@@ -1,30 +1,50 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_ast::{Constant, Expr, ExprKind};
|
||||
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::code_gen::SourceGenerator;
|
||||
use crate::flake8_bugbear::constants::IDENTIFIER_REGEX;
|
||||
use crate::python::keyword::KWLIST;
|
||||
|
||||
static IDENTIFIER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap());
|
||||
fn attribute(value: &Expr, attr: &str) -> Expr {
|
||||
Expr::new(
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
ExprKind::Attribute {
|
||||
value: Box::new(value.clone()),
|
||||
attr: attr.to_string(),
|
||||
ctx: ExprContext::Load,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// B009
|
||||
pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "getattr" {
|
||||
if let [_, arg] = args {
|
||||
if let [obj, arg] = args {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
} = &arg.node
|
||||
{
|
||||
if IDENTIFIER_REGEX.is_match(value) && !KWLIST.contains(&value.as_str()) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::GetAttrWithConstant,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
let mut check =
|
||||
Check::new(CheckKind::GetAttrWithConstant, Range::from_located(expr));
|
||||
if checker.patch() {
|
||||
let mut generator = SourceGenerator::new();
|
||||
if let Ok(()) = generator.unparse_expr(&attribute(obj, value), 0) {
|
||||
if let Ok(content) = generator.generate() {
|
||||
check.amend(Fix::replacement(
|
||||
content,
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
checker.add_check(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub use function_call_argument_default::function_call_argument_default;
|
||||
pub use getattr_with_constant::getattr_with_constant;
|
||||
pub use mutable_argument_default::mutable_argument_default;
|
||||
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
|
||||
pub use setattr_with_constant::setattr_with_constant;
|
||||
pub use star_arg_unpacking_after_keyword_arg::star_arg_unpacking_after_keyword_arg;
|
||||
pub use strip_with_multi_characters::strip_with_multi_characters;
|
||||
pub use unary_prefix_increment::unary_prefix_increment;
|
||||
@@ -24,6 +25,7 @@ mod function_call_argument_default;
|
||||
mod getattr_with_constant;
|
||||
mod mutable_argument_default;
|
||||
mod redundant_tuple_in_exception_handler;
|
||||
mod setattr_with_constant;
|
||||
mod star_arg_unpacking_after_keyword_arg;
|
||||
mod strip_with_multi_characters;
|
||||
mod unary_prefix_increment;
|
||||
|
||||
29
src/flake8_bugbear/plugins/setattr_with_constant.rs
Normal file
29
src/flake8_bugbear/plugins/setattr_with_constant.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use rustpython_ast::{Constant, Expr, ExprKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::flake8_bugbear::constants::IDENTIFIER_REGEX;
|
||||
use crate::python::keyword::KWLIST;
|
||||
|
||||
/// B010
|
||||
pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "setattr" {
|
||||
if let [_, arg, _] = args {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
} = &arg.node
|
||||
{
|
||||
if IDENTIFIER_REGEX.is_match(value) && !KWLIST.contains(&value.as_str()) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SetAttrWithConstant,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,17 +208,18 @@ mod tests {
|
||||
use crate::linter::test_path;
|
||||
use crate::Settings;
|
||||
|
||||
#[test_case(Path::new("reorder_within_section.py"))]
|
||||
#[test_case(Path::new("no_reorder_within_section.py"))]
|
||||
#[test_case(Path::new("separate_future_imports.py"))]
|
||||
#[test_case(Path::new("separate_third_party_imports.py"))]
|
||||
#[test_case(Path::new("separate_first_party_imports.py"))]
|
||||
#[test_case(Path::new("deduplicate_imports.py"))]
|
||||
#[test_case(Path::new("combine_import_froms.py"))]
|
||||
#[test_case(Path::new("preserve_indentation.py"))]
|
||||
#[test_case(Path::new("deduplicate_imports.py"))]
|
||||
#[test_case(Path::new("fit_line_length.py"))]
|
||||
#[test_case(Path::new("import_from_after_import.py"))]
|
||||
#[test_case(Path::new("leading_prefix.py"))]
|
||||
#[test_case(Path::new("no_reorder_within_section.py"))]
|
||||
#[test_case(Path::new("preserve_indentation.py"))]
|
||||
#[test_case(Path::new("reorder_within_section.py"))]
|
||||
#[test_case(Path::new("separate_first_party_imports.py"))]
|
||||
#[test_case(Path::new("separate_future_imports.py"))]
|
||||
#[test_case(Path::new("separate_third_party_imports.py"))]
|
||||
#[test_case(Path::new("skip.py"))]
|
||||
#[test_case(Path::new("trailing_suffix.py"))]
|
||||
fn isort(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}", path.to_string_lossy());
|
||||
|
||||
22
src/isort/snapshots/ruff__isort__tests__skip.py.snap
Normal file
22
src/isort/snapshots/ruff__isort__tests__skip.py.snap
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnsortedImports
|
||||
location:
|
||||
row: 9
|
||||
column: 0
|
||||
end_location:
|
||||
row: 11
|
||||
column: 0
|
||||
fix:
|
||||
patch:
|
||||
content: "import abc\nimport collections\n"
|
||||
location:
|
||||
row: 9
|
||||
column: 0
|
||||
end_location:
|
||||
row: 11
|
||||
column: 0
|
||||
applied: false
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use nohash_hasher::IntSet;
|
||||
use rustpython_ast::{
|
||||
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
|
||||
ExcepthandlerKind, Expr, ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, StmtKind,
|
||||
@@ -8,16 +9,19 @@ use crate::ast::visitor::Visitor;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ImportTracker<'a> {
|
||||
pub blocks: Vec<Vec<&'a Stmt>>,
|
||||
exclusions: &'a IntSet<usize>,
|
||||
blocks: Vec<Vec<&'a Stmt>>,
|
||||
}
|
||||
|
||||
impl<'a> ImportTracker<'a> {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(exclusions: &'a IntSet<usize>) -> Self {
|
||||
Self {
|
||||
exclusions,
|
||||
blocks: vec![vec![]],
|
||||
}
|
||||
}
|
||||
|
||||
fn add_import(&mut self, stmt: &'a Stmt) {
|
||||
fn track_import(&mut self, stmt: &'a Stmt) {
|
||||
let index = self.blocks.len() - 1;
|
||||
self.blocks[index].push(stmt);
|
||||
}
|
||||
@@ -43,8 +47,9 @@ where
|
||||
if matches!(
|
||||
stmt.node,
|
||||
StmtKind::Import { .. } | StmtKind::ImportFrom { .. }
|
||||
) {
|
||||
self.add_import(stmt);
|
||||
) && !self.exclusions.contains(&stmt.location.row())
|
||||
{
|
||||
self.track_import(stmt);
|
||||
} else {
|
||||
self.finalize();
|
||||
}
|
||||
|
||||
11
src/lib.rs
11
src/lib.rs
@@ -25,6 +25,7 @@ pub mod checks_gen;
|
||||
pub mod cli;
|
||||
pub mod code_gen;
|
||||
mod cst;
|
||||
mod directives;
|
||||
mod docstrings;
|
||||
pub mod flake8_annotations;
|
||||
mod flake8_bugbear;
|
||||
@@ -74,8 +75,12 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
|
||||
// Initialize the SourceCodeLocator (which computes offsets lazily).
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
|
||||
// Determine the noqa line for every line in the source.
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
// Extract the `# noqa` and `# isort: skip` directives from the source.
|
||||
let directives = directives::extract_directives(
|
||||
&tokens,
|
||||
&locator,
|
||||
&directives::Flags::from_settings(&settings),
|
||||
);
|
||||
|
||||
// Generate checks.
|
||||
let checks = check_path(
|
||||
@@ -83,7 +88,7 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
|
||||
contents,
|
||||
tokens,
|
||||
&locator,
|
||||
&noqa_line_for,
|
||||
&directives,
|
||||
&settings,
|
||||
&if autofix { Mode::Generate } else { Mode::None },
|
||||
)?;
|
||||
|
||||
@@ -21,11 +21,12 @@ use crate::check_lines::check_lines;
|
||||
use crate::check_tokens::check_tokens;
|
||||
use crate::checks::{Check, CheckCode, CheckKind, LintSource};
|
||||
use crate::code_gen::SourceGenerator;
|
||||
use crate::directives::Directives;
|
||||
use crate::message::Message;
|
||||
use crate::noqa::add_noqa;
|
||||
use crate::settings::Settings;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::{cache, fs, noqa};
|
||||
use crate::{cache, directives, fs};
|
||||
|
||||
/// Collect tokens up to and including the first error.
|
||||
pub(crate) fn tokenize(contents: &str) -> Vec<LexResult> {
|
||||
@@ -56,7 +57,7 @@ pub(crate) fn check_path(
|
||||
contents: &str,
|
||||
tokens: Vec<LexResult>,
|
||||
locator: &SourceCodeLocator,
|
||||
noqa_line_for: &[usize],
|
||||
directives: &Directives,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Vec<Check>> {
|
||||
@@ -88,7 +89,13 @@ pub(crate) fn check_path(
|
||||
checks.extend(check_ast(&python_ast, locator, settings, autofix, path));
|
||||
}
|
||||
if use_imports {
|
||||
checks.extend(check_imports(&python_ast, locator, settings, autofix));
|
||||
checks.extend(check_imports(
|
||||
&python_ast,
|
||||
locator,
|
||||
&directives.isort_exclusions,
|
||||
settings,
|
||||
autofix,
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(parse_error) => {
|
||||
@@ -106,7 +113,13 @@ pub(crate) fn check_path(
|
||||
}
|
||||
|
||||
// Run the lines-based checks.
|
||||
check_lines(&mut checks, contents, noqa_line_for, settings, autofix);
|
||||
check_lines(
|
||||
&mut checks,
|
||||
contents,
|
||||
&directives.noqa_line_for,
|
||||
settings,
|
||||
autofix,
|
||||
);
|
||||
|
||||
// Create path ignores.
|
||||
if !checks.is_empty() && !settings.per_file_ignores.is_empty() {
|
||||
@@ -134,8 +147,12 @@ pub fn lint_stdin(
|
||||
// Initialize the SourceCodeLocator (which computes offsets lazily).
|
||||
let locator = SourceCodeLocator::new(stdin);
|
||||
|
||||
// Determine the noqa line for every line in the source.
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
// Extract the `# noqa` and `# isort: skip` directives from the source.
|
||||
let directives = directives::extract_directives(
|
||||
&tokens,
|
||||
&locator,
|
||||
&directives::Flags::from_settings(settings),
|
||||
);
|
||||
|
||||
// Generate checks.
|
||||
let mut checks = check_path(
|
||||
@@ -143,7 +160,7 @@ pub fn lint_stdin(
|
||||
stdin,
|
||||
tokens,
|
||||
&locator,
|
||||
&noqa_line_for,
|
||||
&directives,
|
||||
settings,
|
||||
autofix,
|
||||
)?;
|
||||
@@ -188,8 +205,12 @@ pub fn lint_path(
|
||||
// Initialize the SourceCodeLocator (which computes offsets lazily).
|
||||
let locator = SourceCodeLocator::new(&contents);
|
||||
|
||||
// Determine the noqa line for every line in the source.
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
// Determine the noqa and isort exclusions.
|
||||
let directives = directives::extract_directives(
|
||||
&tokens,
|
||||
&locator,
|
||||
&directives::Flags::from_settings(settings),
|
||||
);
|
||||
|
||||
// Generate checks.
|
||||
let mut checks = check_path(
|
||||
@@ -197,7 +218,7 @@ pub fn lint_path(
|
||||
&contents,
|
||||
tokens,
|
||||
&locator,
|
||||
&noqa_line_for,
|
||||
&directives,
|
||||
settings,
|
||||
autofix,
|
||||
)?;
|
||||
@@ -230,8 +251,12 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
|
||||
// Initialize the SourceCodeLocator (which computes offsets lazily).
|
||||
let locator = SourceCodeLocator::new(&contents);
|
||||
|
||||
// Determine the noqa line for every line in the source.
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
// Extract the `# noqa` and `# isort: skip` directives from the source.
|
||||
let directives = directives::extract_directives(
|
||||
&tokens,
|
||||
&locator,
|
||||
&directives::Flags::from_settings(settings),
|
||||
);
|
||||
|
||||
// Generate checks.
|
||||
let checks = check_path(
|
||||
@@ -239,12 +264,12 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
|
||||
&contents,
|
||||
tokens,
|
||||
&locator,
|
||||
&noqa_line_for,
|
||||
&directives,
|
||||
settings,
|
||||
&fixer::Mode::None,
|
||||
)?;
|
||||
|
||||
add_noqa(&checks, &contents, &noqa_line_for, path)
|
||||
add_noqa(&checks, &contents, &directives.noqa_line_for, path)
|
||||
}
|
||||
|
||||
pub fn autoformat_path(path: &Path) -> Result<()> {
|
||||
@@ -268,13 +293,17 @@ pub fn test_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Res
|
||||
let contents = fs::read_file(path)?;
|
||||
let tokens: Vec<LexResult> = tokenize(&contents);
|
||||
let locator = SourceCodeLocator::new(&contents);
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
let directives = directives::extract_directives(
|
||||
&tokens,
|
||||
&locator,
|
||||
&directives::Flags::from_settings(settings),
|
||||
);
|
||||
check_path(
|
||||
path,
|
||||
&contents,
|
||||
tokens,
|
||||
&locator,
|
||||
&noqa_line_for,
|
||||
&directives,
|
||||
settings,
|
||||
autofix,
|
||||
)
|
||||
@@ -305,6 +334,7 @@ mod tests {
|
||||
#[test_case(CheckCode::B007, Path::new("B007.py"); "B007")]
|
||||
#[test_case(CheckCode::B008, Path::new("B006_B008.py"); "B008")]
|
||||
#[test_case(CheckCode::B009, Path::new("B009_B010.py"); "B009")]
|
||||
#[test_case(CheckCode::B010, Path::new("B009_B010.py"); "B010")]
|
||||
#[test_case(CheckCode::B011, Path::new("B011.py"); "B011")]
|
||||
#[test_case(CheckCode::B013, Path::new("B013.py"); "B013")]
|
||||
#[test_case(CheckCode::B014, Path::new("B014.py"); "B014")]
|
||||
|
||||
133
src/noqa.rs
133
src/noqa.rs
@@ -3,14 +3,14 @@ use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use nohash_hasher::IntMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::checks::{Check, CheckCode};
|
||||
|
||||
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)(?P<noqa>\s*# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
|
||||
Regex::new(r"(?P<noqa>\s*# 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"));
|
||||
@@ -43,30 +43,21 @@ pub fn extract_noqa_directive(line: &str) -> Directive {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> Vec<usize> {
|
||||
let mut noqa_line_for: Vec<usize> = vec![];
|
||||
for (start, tok, end) in lxr.iter().flatten() {
|
||||
if matches!(tok, Tok::EndOfFile) {
|
||||
break;
|
||||
}
|
||||
// For multi-line strings, we expect `noqa` directives on the last line of the
|
||||
// string. By definition, we can't have multiple multi-line strings on
|
||||
// the same line, so we don't need to verify that we haven't already
|
||||
// traversed past the current line.
|
||||
if matches!(tok, Tok::String { .. }) && end.row() > start.row() {
|
||||
for i in (noqa_line_for.len())..(start.row() - 1) {
|
||||
noqa_line_for.push(i + 1);
|
||||
}
|
||||
noqa_line_for.extend(vec![end.row(); (end.row() + 1) - start.row()]);
|
||||
}
|
||||
}
|
||||
noqa_line_for
|
||||
pub fn add_noqa(
|
||||
checks: &[Check],
|
||||
contents: &str,
|
||||
noqa_line_for: &IntMap<usize, usize>,
|
||||
path: &Path,
|
||||
) -> Result<usize> {
|
||||
let (count, output) = add_noqa_inner(checks, contents, noqa_line_for)?;
|
||||
fs::write(path, output)?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn add_noqa_inner(
|
||||
checks: &[Check],
|
||||
contents: &str,
|
||||
noqa_line_for: &[usize],
|
||||
noqa_line_for: &IntMap<usize, usize>,
|
||||
) -> Result<(usize, String)> {
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
let mut matches_by_line: BTreeMap<usize, BTreeSet<&CheckCode>> = BTreeMap::new();
|
||||
@@ -82,7 +73,7 @@ fn add_noqa_inner(
|
||||
// If there are newlines at the end of the file, they won't be represented in
|
||||
// `noqa_line_for`, so fallback to the current line.
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(lineno)
|
||||
.get(&lineno)
|
||||
.map(|lineno| lineno - 1)
|
||||
.unwrap_or(lineno);
|
||||
|
||||
@@ -120,108 +111,20 @@ fn add_noqa_inner(
|
||||
Ok((count, output))
|
||||
}
|
||||
|
||||
pub fn add_noqa(
|
||||
checks: &[Check],
|
||||
contents: &str,
|
||||
noqa_line_for: &[usize],
|
||||
path: &Path,
|
||||
) -> Result<usize> {
|
||||
let (count, output) = add_noqa_inner(checks, contents, noqa_line_for)?;
|
||||
fs::write(path, output)?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::noqa::{add_noqa_inner, extract_noqa_line_for};
|
||||
|
||||
#[test]
|
||||
fn extraction() -> Result<()> {
|
||||
let empty: Vec<usize> = Default::default();
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"
|
||||
x = 1
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = 2
|
||||
z = x + 1
|
||||
",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
|
||||
y = 2
|
||||
z = x + 1
|
||||
",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = '''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), vec![4, 4, 4, 4]);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = '''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
z = 2",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 5, 5, 5, 5]);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = '''abc
|
||||
def
|
||||
ghi
|
||||
'''",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 5, 5, 5, 5]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use crate::noqa::add_noqa_inner;
|
||||
|
||||
#[test]
|
||||
fn modification() -> Result<()> {
|
||||
let checks = vec![];
|
||||
let contents = "x = 1";
|
||||
let noqa_line_for = vec![1];
|
||||
let noqa_line_for = Default::default();
|
||||
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
|
||||
assert_eq!(count, 0);
|
||||
assert_eq!(output.trim(), contents.trim());
|
||||
@@ -234,7 +137,7 @@ ghi
|
||||
},
|
||||
)];
|
||||
let contents = "x = 1";
|
||||
let noqa_line_for = vec![1];
|
||||
let noqa_line_for = Default::default();
|
||||
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(output.trim(), "x = 1 # noqa: F841".trim());
|
||||
@@ -256,7 +159,7 @@ ghi
|
||||
),
|
||||
];
|
||||
let contents = "x = 1 # noqa: E741";
|
||||
let noqa_line_for = vec![1];
|
||||
let noqa_line_for = Default::default();
|
||||
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());
|
||||
@@ -278,7 +181,7 @@ ghi
|
||||
),
|
||||
];
|
||||
let contents = "x = 1 # noqa";
|
||||
let noqa_line_for = vec![1];
|
||||
let noqa_line_for = Default::default();
|
||||
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());
|
||||
|
||||
@@ -4,34 +4,87 @@ expression: checks
|
||||
---
|
||||
- kind: GetAttrWithConstant
|
||||
location:
|
||||
row: 17
|
||||
row: 18
|
||||
column: 0
|
||||
end_location:
|
||||
row: 17
|
||||
row: 18
|
||||
column: 19
|
||||
fix: ~
|
||||
fix:
|
||||
patch:
|
||||
content: foo.bar
|
||||
location:
|
||||
row: 18
|
||||
column: 0
|
||||
end_location:
|
||||
row: 18
|
||||
column: 19
|
||||
applied: false
|
||||
- kind: GetAttrWithConstant
|
||||
location:
|
||||
row: 18
|
||||
row: 19
|
||||
column: 0
|
||||
end_location:
|
||||
row: 18
|
||||
row: 19
|
||||
column: 23
|
||||
fix: ~
|
||||
fix:
|
||||
patch:
|
||||
content: foo._123abc
|
||||
location:
|
||||
row: 19
|
||||
column: 0
|
||||
end_location:
|
||||
row: 19
|
||||
column: 23
|
||||
applied: false
|
||||
- kind: GetAttrWithConstant
|
||||
location:
|
||||
row: 19
|
||||
row: 20
|
||||
column: 0
|
||||
end_location:
|
||||
row: 19
|
||||
row: 20
|
||||
column: 22
|
||||
fix: ~
|
||||
fix:
|
||||
patch:
|
||||
content: foo.abc123
|
||||
location:
|
||||
row: 20
|
||||
column: 0
|
||||
end_location:
|
||||
row: 20
|
||||
column: 22
|
||||
applied: false
|
||||
- kind: GetAttrWithConstant
|
||||
location:
|
||||
row: 45
|
||||
row: 21
|
||||
column: 0
|
||||
end_location:
|
||||
row: 21
|
||||
column: 23
|
||||
fix:
|
||||
patch:
|
||||
content: foo.abc123
|
||||
location:
|
||||
row: 21
|
||||
column: 0
|
||||
end_location:
|
||||
row: 21
|
||||
column: 23
|
||||
applied: false
|
||||
- kind: GetAttrWithConstant
|
||||
location:
|
||||
row: 22
|
||||
column: 14
|
||||
end_location:
|
||||
row: 45
|
||||
column: 37
|
||||
fix: ~
|
||||
row: 22
|
||||
column: 31
|
||||
fix:
|
||||
patch:
|
||||
content: x.bar
|
||||
location:
|
||||
row: 22
|
||||
column: 14
|
||||
end_location:
|
||||
row: 22
|
||||
column: 31
|
||||
applied: false
|
||||
|
||||
|
||||
37
src/snapshots/ruff__linter__tests__B010_B009_B010.py.snap
Normal file
37
src/snapshots/ruff__linter__tests__B010_B009_B010.py.snap
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 33
|
||||
column: 0
|
||||
end_location:
|
||||
row: 33
|
||||
column: 25
|
||||
fix: ~
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 34
|
||||
column: 0
|
||||
end_location:
|
||||
row: 34
|
||||
column: 29
|
||||
fix: ~
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 35
|
||||
column: 0
|
||||
end_location:
|
||||
row: 35
|
||||
column: 28
|
||||
fix: ~
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 36
|
||||
column: 0
|
||||
end_location:
|
||||
row: 36
|
||||
column: 29
|
||||
fix: ~
|
||||
|
||||
@@ -17,7 +17,7 @@ impl<'a> SourceCodeLocator<'a> {
|
||||
pub fn new(contents: &'a str) -> Self {
|
||||
SourceCodeLocator {
|
||||
contents,
|
||||
rope: OnceCell::new(),
|
||||
rope: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user