Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05fbd1a283 | ||
|
|
63552cbc8e | ||
|
|
c00bd489f1 | ||
|
|
16c2e3a995 | ||
|
|
650b025181 | ||
|
|
8fe46f7400 | ||
|
|
a9bcc15797 | ||
|
|
b6c856bd07 | ||
|
|
4beea0484a | ||
|
|
2679db1d10 | ||
|
|
3e73462e04 | ||
|
|
f63a87737a | ||
|
|
e7472eac1c | ||
|
|
db3c847771 | ||
|
|
4adbfc24a5 | ||
|
|
8f734a6562 | ||
|
|
bcf7519eb3 | ||
|
|
66089052ee | ||
|
|
d50cc8ff65 | ||
|
|
b75ea94f58 | ||
|
|
2c24e2fd28 | ||
|
|
f8dc208665 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2045,7 +2045,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.82"
|
||||
version = "0.0.85"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.82"
|
||||
version = "0.0.85"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
31
README.md
31
README.md
@@ -77,7 +77,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.82
|
||||
rev: v0.0.85
|
||||
hooks:
|
||||
- id: lint
|
||||
```
|
||||
@@ -257,7 +257,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
|
||||
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | |
|
||||
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | |
|
||||
|
||||
### pycodestyle
|
||||
### pycodestyle (error)
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
@@ -275,7 +275,13 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
|
||||
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | |
|
||||
| E902 | IOError | IOError: `...` | |
|
||||
| E999 | SyntaxError | SyntaxError: `...` | |
|
||||
|
||||
### pycodestyle (warning)
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
|
||||
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | |
|
||||
|
||||
### pydocstyle
|
||||
|
||||
@@ -348,12 +354,12 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
|
||||
| N803 | InvalidArgumentName | Argument name `...` should be lowercase | |
|
||||
| N804 | InvalidFirstArgumentNameForClassMethod | First argument of a class method should be named `cls` | |
|
||||
| N805 | InvalidFirstArgumentNameForMethod | First argument of a method should be named `self` | |
|
||||
| N807 | DunderFunctionName | function name should not start and end with '__' | |
|
||||
| N811 | ConstantImportedAsNonConstant | constant '...' imported as non constant '...' | |
|
||||
| N812 | LowercaseImportedAsNonLowercase | lowercase '...' imported as non lowercase '...' | |
|
||||
| N813 | CamelcaseImportedAsLowercase | camelcase '...' imported as lowercase '...' | |
|
||||
| N814 | CamelcaseImportedAsConstant | camelcase '...' imported as constant '...' | |
|
||||
| N817 | CamelcaseImportedAsAcronym | camelcase '...' imported as acronym '...' | |
|
||||
| N807 | DunderFunctionName | Function name should not start and end with `__` | |
|
||||
| N811 | ConstantImportedAsNonConstant | Constant `...` imported as non-constant `...` | |
|
||||
| N812 | LowercaseImportedAsNonLowercase | Lowercase `...` imported as non-lowercase `...` | |
|
||||
| N813 | CamelcaseImportedAsLowercase | Camelcase `...` imported as lowercase `...` | |
|
||||
| N814 | CamelcaseImportedAsConstant | Camelcase `...` imported as constant `...` | |
|
||||
| N817 | CamelcaseImportedAsAcronym | Camelcase `...` imported as acronym `...` | |
|
||||
|
||||
### flake8-comprehensions
|
||||
|
||||
@@ -380,8 +386,11 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment. | |
|
||||
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 |
|
||||
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
|
||||
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
|
||||
| B017 | NoAssertRaisesException | `assertRaises(Exception):` should be considered evil. | |
|
||||
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
|
||||
|
||||
### flake8-builtins
|
||||
@@ -471,7 +480,7 @@ including:
|
||||
- [`flake8-super`](https://pypi.org/project/flake8-super/)
|
||||
- [`flake8-print`](https://pypi.org/project/flake8-print/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (9/32)
|
||||
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
|
||||
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
|
||||
|
||||
@@ -491,7 +500,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
|
||||
- [`flake8-super`](https://pypi.org/project/flake8-super/)
|
||||
- [`flake8-print`](https://pypi.org/project/flake8-print/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (9/32)
|
||||
|
||||
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
|
||||
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34).
|
||||
@@ -512,7 +521,7 @@ on Rust at all.
|
||||
### Can I write my own plugins for Ruff?
|
||||
|
||||
Ruff does not yet support third-party plugins, though a plugin system is within-scope for the
|
||||
project. See [#283](https://github.com/charliermarsh/ruff/issues/2830) for more.
|
||||
project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
|
||||
|
||||
### Does Ruff support NumPy- or Google-style docstrings?
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ fn main() {
|
||||
"| {} | {} | {} | {} |",
|
||||
check_kind.code().as_ref(),
|
||||
check_kind.as_ref(),
|
||||
check_kind.body().replace("|", r"\|"),
|
||||
check_kind.summary().replace("|", r"\|"),
|
||||
fix_token
|
||||
);
|
||||
}
|
||||
|
||||
20
resources/test/fixtures/B002.py
vendored
Normal file
20
resources/test/fixtures/B002.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Should emit:
|
||||
B002 - on lines 15 and 20
|
||||
"""
|
||||
|
||||
|
||||
def this_is_all_fine(n):
|
||||
x = n + 1
|
||||
y = 1 + n
|
||||
z = +x + y
|
||||
return +z
|
||||
|
||||
|
||||
def this_is_buggy(n):
|
||||
x = ++n
|
||||
return x
|
||||
|
||||
|
||||
def this_is_buggy_too(n):
|
||||
return ++n
|
||||
31
resources/test/fixtures/B007.py
vendored
Normal file
31
resources/test/fixtures/B007.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
for i in range(10):
|
||||
print(i)
|
||||
|
||||
print(i) # name no longer defined on Python 3; no warning yet
|
||||
|
||||
for i in range(10): # name not used within the loop; B007
|
||||
print(10)
|
||||
|
||||
print(i) # name no longer defined on Python 3; no warning yet
|
||||
|
||||
|
||||
for _ in range(10): # _ is okay for a throw-away variable
|
||||
print(10)
|
||||
|
||||
|
||||
for i in range(10):
|
||||
for j in range(10):
|
||||
for k in range(10): # k not used, i and j used transitively
|
||||
print(i + j)
|
||||
|
||||
|
||||
def strange_generator():
|
||||
for i in range(10):
|
||||
for j in range(10):
|
||||
for k in range(10):
|
||||
for l in range(10):
|
||||
yield i, (j, (k, l))
|
||||
|
||||
|
||||
for i, (j, (k, l)) in strange_generator(): # i, k not used
|
||||
print(j, l)
|
||||
36
resources/test/fixtures/B017.py
vendored
Normal file
36
resources/test/fixtures/B017.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Should emit:
|
||||
B017 - on lines 20
|
||||
"""
|
||||
import asyncio
|
||||
import unittest
|
||||
|
||||
CONSTANT = True
|
||||
|
||||
|
||||
def something_else() -> None:
|
||||
for i in (1, 2, 3):
|
||||
print(i)
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
class Foobar(unittest.TestCase):
|
||||
def evil_raises(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
raise Exception("Evil I say!")
|
||||
|
||||
def context_manager_raises(self) -> None:
|
||||
with self.assertRaises(Exception) as ex:
|
||||
raise Exception("Context manager is good")
|
||||
self.assertEqual("Context manager is good", str(ex.exception))
|
||||
|
||||
def regex_raises(self) -> None:
|
||||
with self.assertRaisesRegex(Exception, "Regex is good"):
|
||||
raise Exception("Regex is good")
|
||||
|
||||
def raises_with_absolute_reference(self):
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
Foo()
|
||||
5
resources/test/fixtures/F401_5.py
vendored
Normal file
5
resources/test/fixtures/F401_5.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Test: removal of multi-segment and aliases imports."""
|
||||
from a.b import c
|
||||
from d.e import f as g
|
||||
import h.i
|
||||
import j.k as l
|
||||
4
resources/test/fixtures/N811.py
vendored
4
resources/test/fixtures/N811.py
vendored
@@ -1 +1,3 @@
|
||||
from mod import BAD as bad
|
||||
import mod.CONST as const
|
||||
from mod import CONSTANT as constant
|
||||
from mod import ANOTHER_CONSTANT as another_constant
|
||||
|
||||
4
resources/test/fixtures/N812.py
vendored
4
resources/test/fixtures/N812.py
vendored
@@ -1 +1,3 @@
|
||||
from mod import bad as Bad
|
||||
import modl.lowercase as Lower
|
||||
from mod import lowercase as Lowercase
|
||||
from mod import another_lowercase as AnotherLowercase
|
||||
|
||||
2
resources/test/fixtures/N813.py
vendored
2
resources/test/fixtures/N813.py
vendored
@@ -1 +1,3 @@
|
||||
import mod.Camel as camel
|
||||
from mod import CamelCase as camelcase
|
||||
from mod import AnotherCamelCase as another_camelcase
|
||||
|
||||
2
resources/test/fixtures/N814.py
vendored
2
resources/test/fixtures/N814.py
vendored
@@ -1 +1,3 @@
|
||||
import mod.Camel as CAMEL
|
||||
from mod import CamelCase as CAMELCASE
|
||||
from mod import AnotherCamelCase as ANOTHER_CAMELCASE
|
||||
|
||||
1
resources/test/fixtures/N817.py
vendored
1
resources/test/fixtures/N817.py
vendored
@@ -1 +1,2 @@
|
||||
import mod.CaMel as CM
|
||||
from mod import CamelCase as CC
|
||||
|
||||
35
resources/test/fixtures/W605.py
vendored
Normal file
35
resources/test/fixtures/W605.py
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
#: W605:1:10
|
||||
regex = '\.png$'
|
||||
|
||||
#: W605:2:1
|
||||
regex = '''
|
||||
\.png$
|
||||
'''
|
||||
|
||||
#: W605:2:6
|
||||
f(
|
||||
'\_'
|
||||
)
|
||||
|
||||
#: W605:4:6
|
||||
"""
|
||||
multi-line
|
||||
literal
|
||||
with \_ somewhere
|
||||
in the middle
|
||||
"""
|
||||
|
||||
#: Okay
|
||||
regex = r'\.png$'
|
||||
regex = '\\.png$'
|
||||
regex = r'''
|
||||
\.png$
|
||||
'''
|
||||
regex = r'''
|
||||
\\.png$
|
||||
'''
|
||||
s = '\\'
|
||||
regex = '\w' # noqa
|
||||
regex = '''
|
||||
\w
|
||||
''' # noqa
|
||||
@@ -122,56 +122,50 @@ pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
|
||||
pub struct SourceCodeLocator<'a> {
|
||||
content: &'a str,
|
||||
offsets: Vec<Vec<usize>>,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl<'a> SourceCodeLocator<'a> {
|
||||
pub fn new(content: &'a str) -> Self {
|
||||
SourceCodeLocator {
|
||||
content,
|
||||
offsets: vec![],
|
||||
initialized: false,
|
||||
offsets: Self::compute_offsets(content),
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
if !self.initialized {
|
||||
let mut offset = 0;
|
||||
for line in self.content.lines() {
|
||||
let mut newline = 0;
|
||||
let mut line_offsets: Vec<usize> = vec![];
|
||||
for (i, _char) in line.char_indices() {
|
||||
line_offsets.push(offset + i);
|
||||
newline = i + 1;
|
||||
}
|
||||
line_offsets.push(offset + newline);
|
||||
self.offsets.push(line_offsets);
|
||||
offset += newline + 1;
|
||||
fn compute_offsets(content: &str) -> Vec<Vec<usize>> {
|
||||
let mut offsets = vec![];
|
||||
let mut offset = 0;
|
||||
for line in content.lines() {
|
||||
let mut newline = 0;
|
||||
let mut line_offsets: Vec<usize> = vec![];
|
||||
for (i, char) in line.char_indices() {
|
||||
line_offsets.push(offset + i);
|
||||
newline = i + char.len_utf8();
|
||||
}
|
||||
self.offsets.push(vec![offset]);
|
||||
self.initialized = true;
|
||||
line_offsets.push(offset + newline);
|
||||
offsets.push(line_offsets);
|
||||
offset += newline + 1;
|
||||
}
|
||||
offsets.push(vec![offset]);
|
||||
offsets
|
||||
}
|
||||
|
||||
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
|
||||
self.init();
|
||||
pub fn slice_source_code_at(&self, location: &Location) -> &'a str {
|
||||
let offset = self.offsets[location.row() - 1][location.column() - 1];
|
||||
&self.content[offset..]
|
||||
}
|
||||
|
||||
pub fn slice_source_code_range(&mut self, range: &Range) -> &'a str {
|
||||
self.init();
|
||||
pub fn slice_source_code_range(&self, range: &Range) -> &'a str {
|
||||
let start = self.offsets[range.location.row() - 1][range.location.column() - 1];
|
||||
let end = self.offsets[range.end_location.row() - 1][range.end_location.column() - 1];
|
||||
&self.content[start..end]
|
||||
}
|
||||
|
||||
pub fn partition_source_code_at(
|
||||
&mut self,
|
||||
&self,
|
||||
outer: &Range,
|
||||
inner: &Range,
|
||||
) -> (&'a str, &'a str, &'a str) {
|
||||
self.init();
|
||||
let outer_start = self.offsets[outer.location.row() - 1][outer.location.column() - 1];
|
||||
let outer_end = self.offsets[outer.end_location.row() - 1][outer.end_location.column() - 1];
|
||||
let inner_start = self.offsets[inner.location.row() - 1][inner.location.column() - 1];
|
||||
@@ -183,3 +177,22 @@ impl<'a> SourceCodeLocator<'a> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SourceCodeLocator;
|
||||
|
||||
#[test]
|
||||
fn source_code_locator_init() {
|
||||
let content = "# \u{4e9c}\nclass Foo:\n \"\"\".\"\"\"";
|
||||
let locator = SourceCodeLocator::new(content);
|
||||
assert_eq!(locator.offsets.len(), 4);
|
||||
assert_eq!(locator.offsets[0], [0, 1, 2, 5]);
|
||||
assert_eq!(locator.offsets[1], [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
||||
assert_eq!(
|
||||
locator.offsets[2],
|
||||
[17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
|
||||
);
|
||||
assert_eq!(locator.offsets[3], [29]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::autofix::Fix;
|
||||
use crate::autofix::Patch;
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::Location;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::Fix;
|
||||
use crate::autofix::Patch;
|
||||
use crate::checks::Check;
|
||||
|
||||
#[derive(Hash)]
|
||||
@@ -13,6 +14,17 @@ pub enum Mode {
|
||||
None,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// Return `true` if a patch should be generated under the given `Mode`.
|
||||
pub fn patch(&self) -> bool {
|
||||
match &self {
|
||||
Mode::Generate => true,
|
||||
Mode::Apply => true,
|
||||
Mode::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Lint rules based on AST traversal.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
|
||||
use log::error;
|
||||
use once_cell::unsync::OnceCell;
|
||||
use rustpython_parser::ast::{
|
||||
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
|
||||
KeywordData, Operator, Stmt, StmtKind, Suite,
|
||||
@@ -34,12 +37,14 @@ pub const GLOBAL_SCOPE_INDEX: usize = 0;
|
||||
|
||||
pub struct Checker<'a> {
|
||||
// Input data.
|
||||
pub(crate) path: &'a Path,
|
||||
pub(crate) locator: SourceCodeLocator<'a>,
|
||||
path: &'a Path,
|
||||
content: &'a str,
|
||||
autofix: &'a fixer::Mode,
|
||||
pub(crate) settings: &'a Settings,
|
||||
pub(crate) autofix: &'a fixer::Mode,
|
||||
// Computed checks.
|
||||
checks: Vec<Check>,
|
||||
// Efficient source-code slicing.
|
||||
locator: OnceCell<SourceCodeLocator<'a>>,
|
||||
// Docstring tracking.
|
||||
docstrings: Vec<(Definition<'a>, Visibility)>,
|
||||
// Edit tracking.
|
||||
@@ -79,7 +84,8 @@ impl<'a> Checker<'a> {
|
||||
settings,
|
||||
autofix,
|
||||
path,
|
||||
locator: SourceCodeLocator::new(content),
|
||||
content,
|
||||
locator: OnceCell::new(),
|
||||
checks: Default::default(),
|
||||
docstrings: Default::default(),
|
||||
deletions: Default::default(),
|
||||
@@ -106,6 +112,17 @@ impl<'a> Checker<'a> {
|
||||
except_handlers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get access to a lazily-initialized `SourceCodeLocator` for the file contents.
|
||||
pub fn get_locator(&self) -> &SourceCodeLocator {
|
||||
self.locator
|
||||
.get_or_init(|| SourceCodeLocator::new(self.content))
|
||||
}
|
||||
|
||||
/// Return `true` if a patch should be generated under the given autofix `Mode`.
|
||||
pub fn patch(&self) -> bool {
|
||||
self.autofix.patch()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for Checker<'a>
|
||||
@@ -413,6 +430,55 @@ where
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(asname) = &alias.node.asname {
|
||||
let name = alias.node.name.split('.').last().unwrap();
|
||||
if self.settings.enabled.contains(&CheckCode::N811) {
|
||||
if let Some(check) =
|
||||
pep8_naming::checks::constant_imported_as_non_constant(
|
||||
stmt, name, asname,
|
||||
)
|
||||
{
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.enabled.contains(&CheckCode::N812) {
|
||||
if let Some(check) =
|
||||
pep8_naming::checks::lowercase_imported_as_non_lowercase(
|
||||
stmt, name, asname,
|
||||
)
|
||||
{
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.enabled.contains(&CheckCode::N813) {
|
||||
if let Some(check) =
|
||||
pep8_naming::checks::camelcase_imported_as_lowercase(
|
||||
stmt, name, asname,
|
||||
)
|
||||
{
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.enabled.contains(&CheckCode::N814) {
|
||||
if let Some(check) = pep8_naming::checks::camelcase_imported_as_constant(
|
||||
stmt, name, asname,
|
||||
) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.enabled.contains(&CheckCode::N817) {
|
||||
if let Some(check) = pep8_naming::checks::camelcase_imported_as_acronym(
|
||||
stmt, name, asname,
|
||||
) {
|
||||
self.checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::ImportFrom {
|
||||
@@ -618,6 +684,16 @@ where
|
||||
flake8_bugbear::plugins::assert_false(self, stmt, test, msg);
|
||||
}
|
||||
}
|
||||
StmtKind::With { items, .. } | StmtKind::AsyncWith { items, .. } => {
|
||||
if self.settings.enabled.contains(&CheckCode::B017) {
|
||||
flake8_bugbear::plugins::assert_raises_exception(self, stmt, items);
|
||||
}
|
||||
}
|
||||
StmtKind::For { target, body, .. } => {
|
||||
if self.settings.enabled.contains(&CheckCode::B007) {
|
||||
flake8_bugbear::plugins::unused_loop_control_variable(self, target, body);
|
||||
}
|
||||
}
|
||||
StmtKind::Try { handlers, .. } => {
|
||||
if self.settings.enabled.contains(&CheckCode::F707) {
|
||||
if let Some(check) = pyflakes::checks::default_except_not_last(handlers) {
|
||||
@@ -1077,6 +1153,10 @@ where
|
||||
self,
|
||||
));
|
||||
}
|
||||
|
||||
if self.settings.enabled.contains(&CheckCode::B002) {
|
||||
flake8_bugbear::plugins::unary_prefix_increment(self, expr, op, operand);
|
||||
}
|
||||
}
|
||||
ExprKind::Compare {
|
||||
left,
|
||||
@@ -2012,8 +2092,7 @@ impl<'a> Checker<'a> {
|
||||
let child = self.parents[defined_by];
|
||||
let parent = defined_in.map(|defined_in| self.parents[defined_in]);
|
||||
|
||||
let fix = if matches!(self.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
|
||||
{
|
||||
let fix = if self.patch() {
|
||||
let deleted: Vec<&Stmt> = self
|
||||
.deletions
|
||||
.iter()
|
||||
@@ -2025,7 +2104,7 @@ impl<'a> Checker<'a> {
|
||||
ImportKind::ImportFrom => pyflakes::fixes::remove_unused_import_froms,
|
||||
};
|
||||
|
||||
match removal_fn(&mut self.locator, &full_names, child, parent, &deleted) {
|
||||
match removal_fn(self.get_locator(), &full_names, child, parent, &deleted) {
|
||||
Ok(fix) => Some(fix),
|
||||
Err(e) => {
|
||||
error!("Failed to fix unused imports: {}", e);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Lint rules based on checking raw physical lines.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rustpython_parser::ast::Location;
|
||||
@@ -166,7 +168,7 @@ pub fn check_lines(
|
||||
end_location: Location::new(row + 1, end + 1),
|
||||
},
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if autofix.patch() {
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(row + 1, start + 1),
|
||||
Location::new(row + 1, lines[row].chars().count() + 1),
|
||||
@@ -194,7 +196,7 @@ pub fn check_lines(
|
||||
end_location: Location::new(row + 1, end + 1),
|
||||
},
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if autofix.patch() {
|
||||
if valid_codes.is_empty() {
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(row + 1, start + 1),
|
||||
|
||||
124
src/check_tokens.rs
Normal file
124
src/check_tokens.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
//! Lint rules based on token traversal.
|
||||
|
||||
use rustpython_ast::Location;
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::ast::operations::SourceCodeLocator;
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::Settings;
|
||||
|
||||
// See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
|
||||
const VALID_ESCAPE_SEQUENCES: &[char; 23] = &[
|
||||
'\n', '\\', '\'', '"', 'a', 'b', 'f', 'n', 'r', 't', 'v', '0', '1', '2', '3', '4', '5', '6',
|
||||
'7', 'x', // Escape sequences only recognized in string literals
|
||||
'N', 'u', 'U',
|
||||
];
|
||||
|
||||
/// Return the quotation markers used for a String token.
|
||||
fn extract_quote(text: &str) -> &str {
|
||||
if text.len() >= 3 {
|
||||
let triple = &text[text.len() - 3..];
|
||||
if triple == "'''" || triple == "\"\"\"" {
|
||||
return triple;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
let single = &text[text.len() - 1..];
|
||||
if single == "'" || single == "\"" {
|
||||
return single;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Unable to find quotation mark for String token.")
|
||||
}
|
||||
|
||||
/// W605
|
||||
fn invalid_escape_sequence(
|
||||
locator: &SourceCodeLocator,
|
||||
start: &Location,
|
||||
end: &Location,
|
||||
) -> Vec<Check> {
|
||||
let mut checks = vec![];
|
||||
|
||||
let text = locator.slice_source_code_range(&Range {
|
||||
location: *start,
|
||||
end_location: *end,
|
||||
});
|
||||
|
||||
// Determine whether the string is single- or triple-quoted.
|
||||
let quote = extract_quote(text);
|
||||
let quote_pos = text.find(quote).unwrap();
|
||||
let prefix = text[..quote_pos].to_lowercase();
|
||||
let body = &text[(quote_pos + quote.len())..(text.len() - quote.len())];
|
||||
|
||||
if !prefix.contains('r') {
|
||||
let mut col_offset = 0;
|
||||
let mut row_offset = 0;
|
||||
let mut in_escape = false;
|
||||
let mut chars = body.chars();
|
||||
let mut current = chars.next();
|
||||
let mut next = chars.next();
|
||||
while let (Some(current_char), Some(next_char)) = (current, next) {
|
||||
// If we see an escaped backslash, avoid treating the character _after_ the
|
||||
// escaped backslash as itself an escaped character.
|
||||
if in_escape {
|
||||
in_escape = false;
|
||||
} else {
|
||||
in_escape = current_char == '\\' && next_char == '\\';
|
||||
if current_char == '\\' && !VALID_ESCAPE_SEQUENCES.contains(&next_char) {
|
||||
// Compute the location of the escape sequence by offsetting the location of the
|
||||
// string token by the characters we've seen thus far.
|
||||
let location = if row_offset == 0 {
|
||||
Location::new(
|
||||
start.row() + row_offset,
|
||||
start.column() + prefix.len() + quote.len() + col_offset,
|
||||
)
|
||||
} else {
|
||||
Location::new(start.row() + row_offset, col_offset + 1)
|
||||
};
|
||||
let end_location = Location::new(location.row(), location.column() + 1);
|
||||
checks.push(Check::new(
|
||||
CheckKind::InvalidEscapeSequence(next_char),
|
||||
Range {
|
||||
location,
|
||||
end_location,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Track the offset from the start position as we iterate over the body.
|
||||
if current_char == '\n' {
|
||||
col_offset = 0;
|
||||
row_offset += 1;
|
||||
} else {
|
||||
col_offset += 1;
|
||||
}
|
||||
|
||||
current = next;
|
||||
next = chars.next();
|
||||
}
|
||||
}
|
||||
|
||||
checks
|
||||
}
|
||||
|
||||
pub fn check_tokens(
|
||||
checks: &mut Vec<Check>,
|
||||
contents: &str,
|
||||
tokens: &[LexResult],
|
||||
settings: &Settings,
|
||||
) {
|
||||
// TODO(charlie): Use a shared SourceCodeLocator between this site and the AST traversal.
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let enforce_invalid_escape_sequence = settings.enabled.contains(&CheckCode::W605);
|
||||
for (start, tok, end) in tokens.iter().flatten() {
|
||||
if enforce_invalid_escape_sequence {
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
checks.extend(invalid_escape_sequence(&locator, start, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ pub enum CheckCode {
|
||||
E999,
|
||||
// pycodestyle warnings
|
||||
W292,
|
||||
W605,
|
||||
// pyflakes
|
||||
F401,
|
||||
F402,
|
||||
@@ -75,8 +76,11 @@ pub enum CheckCode {
|
||||
A002,
|
||||
A003,
|
||||
// flake8-bugbear
|
||||
B002,
|
||||
B007,
|
||||
B011,
|
||||
B014,
|
||||
B017,
|
||||
B025,
|
||||
// flake8-comprehensions
|
||||
C400,
|
||||
@@ -171,7 +175,8 @@ pub enum CheckCode {
|
||||
#[derive(EnumIter, Debug, PartialEq, Eq)]
|
||||
pub enum CheckCategory {
|
||||
Pyflakes,
|
||||
Pycodestyle,
|
||||
PycodestyleError,
|
||||
PycodestyleWarning,
|
||||
Pydocstyle,
|
||||
Pyupgrade,
|
||||
PEP8Naming,
|
||||
@@ -185,7 +190,8 @@ pub enum CheckCategory {
|
||||
impl CheckCategory {
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self {
|
||||
CheckCategory::Pycodestyle => "pycodestyle",
|
||||
CheckCategory::PycodestyleError => "pycodestyle (error)",
|
||||
CheckCategory::PycodestyleWarning => "pycodestyle (warning)",
|
||||
CheckCategory::Pyflakes => "Pyflakes",
|
||||
CheckCategory::Flake8Builtins => "flake8-builtins",
|
||||
CheckCategory::Flake8Bugbear => "flake8-bugbear",
|
||||
@@ -202,8 +208,9 @@ impl CheckCategory {
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum LintSource {
|
||||
AST,
|
||||
Lines,
|
||||
FileSystem,
|
||||
Lines,
|
||||
Tokens,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -231,6 +238,7 @@ pub enum CheckKind {
|
||||
TypeComparison,
|
||||
// pycodestyle warnings
|
||||
NoNewLineAtEndOfFile,
|
||||
InvalidEscapeSequence(char),
|
||||
// pyflakes
|
||||
AssertTuple,
|
||||
BreakOutsideLoop,
|
||||
@@ -265,8 +273,11 @@ pub enum CheckKind {
|
||||
BuiltinArgumentShadowing(String),
|
||||
BuiltinAttributeShadowing(String),
|
||||
// flake8-bugbear
|
||||
UnaryPrefixIncrement,
|
||||
UnusedLoopControlVariable(String),
|
||||
DoNotAssertFalse,
|
||||
DuplicateHandlerException(Vec<String>),
|
||||
NoAssertRaisesException,
|
||||
DuplicateTryBlockException(String),
|
||||
// flake8-comprehensions
|
||||
UnnecessaryGeneratorList,
|
||||
@@ -363,6 +374,7 @@ impl CheckCode {
|
||||
pub fn lint_source(&self) -> &'static LintSource {
|
||||
match self {
|
||||
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 => &LintSource::Lines,
|
||||
CheckCode::W605 => &LintSource::Tokens,
|
||||
CheckCode::E902 => &LintSource::FileSystem,
|
||||
_ => &LintSource::AST,
|
||||
}
|
||||
@@ -388,6 +400,7 @@ impl CheckCode {
|
||||
CheckCode::E999 => CheckKind::SyntaxError("`...`".to_string()),
|
||||
// pycodestyle warnings
|
||||
CheckCode::W292 => CheckKind::NoNewLineAtEndOfFile,
|
||||
CheckCode::W605 => CheckKind::InvalidEscapeSequence('c'),
|
||||
// pyflakes
|
||||
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()]),
|
||||
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
|
||||
@@ -424,8 +437,11 @@ impl CheckCode {
|
||||
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
|
||||
CheckCode::A003 => CheckKind::BuiltinAttributeShadowing("...".to_string()),
|
||||
// flake8-bugbear
|
||||
CheckCode::B002 => CheckKind::UnaryPrefixIncrement,
|
||||
CheckCode::B007 => CheckKind::UnusedLoopControlVariable("i".to_string()),
|
||||
CheckCode::B011 => CheckKind::DoNotAssertFalse,
|
||||
CheckCode::B014 => CheckKind::DuplicateHandlerException(vec!["ValueError".to_string()]),
|
||||
CheckCode::B017 => CheckKind::NoAssertRaisesException,
|
||||
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
|
||||
// flake8-comprehensions
|
||||
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
|
||||
@@ -552,21 +568,22 @@ impl CheckCode {
|
||||
|
||||
pub fn category(&self) -> CheckCategory {
|
||||
match self {
|
||||
CheckCode::E402 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E501 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E711 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E712 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E713 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E714 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E721 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E722 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E731 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E741 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E742 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E743 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E902 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E999 => CheckCategory::Pycodestyle,
|
||||
CheckCode::W292 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E402 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E501 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E711 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E712 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E713 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E714 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E721 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E722 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E731 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E741 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E742 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E743 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E902 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E999 => CheckCategory::PycodestyleError,
|
||||
CheckCode::W292 => CheckCategory::PycodestyleWarning,
|
||||
CheckCode::W605 => CheckCategory::PycodestyleWarning,
|
||||
CheckCode::F401 => CheckCategory::Pyflakes,
|
||||
CheckCode::F402 => CheckCategory::Pyflakes,
|
||||
CheckCode::F403 => CheckCategory::Pyflakes,
|
||||
@@ -598,8 +615,11 @@ impl CheckCode {
|
||||
CheckCode::A001 => CheckCategory::Flake8Builtins,
|
||||
CheckCode::A002 => CheckCategory::Flake8Builtins,
|
||||
CheckCode::A003 => CheckCategory::Flake8Builtins,
|
||||
CheckCode::B002 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B007 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B011 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B014 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B017 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B025 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
|
||||
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
|
||||
@@ -736,13 +756,17 @@ impl CheckKind {
|
||||
CheckKind::YieldOutsideFunction => &CheckCode::F704,
|
||||
// pycodestyle warnings
|
||||
CheckKind::NoNewLineAtEndOfFile => &CheckCode::W292,
|
||||
CheckKind::InvalidEscapeSequence(_) => &CheckCode::W605,
|
||||
// flake8-builtins
|
||||
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
|
||||
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
|
||||
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
|
||||
// flake8-bugbear
|
||||
CheckKind::UnaryPrefixIncrement => &CheckCode::B002,
|
||||
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
|
||||
CheckKind::DoNotAssertFalse => &CheckCode::B011,
|
||||
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
|
||||
CheckKind::NoAssertRaisesException => &CheckCode::B017,
|
||||
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
|
||||
// flake8-comprehensions
|
||||
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
|
||||
@@ -968,6 +992,7 @@ impl CheckKind {
|
||||
}
|
||||
// pycodestyle warnings
|
||||
CheckKind::NoNewLineAtEndOfFile => "No newline at end of file".to_string(),
|
||||
CheckKind::InvalidEscapeSequence(char) => format!("Invalid escape sequence: '\\{char}'"),
|
||||
// flake8-builtins
|
||||
CheckKind::BuiltinVariableShadowing(name) => {
|
||||
format!("Variable `{name}` is shadowing a python builtin")
|
||||
@@ -979,6 +1004,8 @@ impl CheckKind {
|
||||
format!("Class attribute `{name}` is shadowing a python builtin")
|
||||
}
|
||||
// flake8-bugbear
|
||||
CheckKind::UnaryPrefixIncrement => "Python does not support the unary prefix increment. Writing `++n` is equivalent to `+(+(n))`, which equals `n`. You meant `n += 1`.".to_string(),
|
||||
CheckKind::UnusedLoopControlVariable(name) => format!("Loop control variable `{name}` not used within the loop body. If this is intended, start the name with an underscore."),
|
||||
CheckKind::DoNotAssertFalse => {
|
||||
"Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`"
|
||||
.to_string()
|
||||
@@ -992,6 +1019,9 @@ impl CheckKind {
|
||||
format!("Exception handler with duplicate exceptions: {names}")
|
||||
}
|
||||
}
|
||||
CheckKind::NoAssertRaisesException => {
|
||||
"`assertRaises(Exception):` should be considered evil. It can lead to your test passing even if the code being tested is never executed due to a typo. Either assert for a more specific exception (builtin or custom), use `assertRaisesRegex`, or use the context manager form of `assertRaises`.".to_string()
|
||||
}
|
||||
CheckKind::DuplicateTryBlockException(name) => {
|
||||
format!("try-except block with duplicate exception `{name}`")
|
||||
}
|
||||
@@ -1221,22 +1251,22 @@ impl CheckKind {
|
||||
"First argument of a method should be named `self`".to_string()
|
||||
}
|
||||
CheckKind::DunderFunctionName => {
|
||||
"function name should not start and end with '__'".to_string()
|
||||
"Function name should not start and end with `__`".to_string()
|
||||
}
|
||||
CheckKind::ConstantImportedAsNonConstant(name, asname) => {
|
||||
format!("constant '{name}' imported as non constant '{asname}'")
|
||||
format!("Constant `{name}` imported as non-constant `{asname}`")
|
||||
}
|
||||
CheckKind::LowercaseImportedAsNonLowercase(name, asname) => {
|
||||
format!("lowercase '{name}' imported as non lowercase '{asname}'")
|
||||
format!("Lowercase `{name}` imported as non-lowercase `{asname}`")
|
||||
}
|
||||
CheckKind::CamelcaseImportedAsLowercase(name, asname) => {
|
||||
format!("camelcase '{name}' imported as lowercase '{asname}'")
|
||||
format!("Camelcase `{name}` imported as lowercase `{asname}`")
|
||||
}
|
||||
CheckKind::CamelcaseImportedAsConstant(name, asname) => {
|
||||
format!("camelcase '{name}' imported as constant '{asname}'")
|
||||
format!("Camelcase `{name}` imported as constant `{asname}`")
|
||||
}
|
||||
CheckKind::CamelcaseImportedAsAcronym(name, asname) => {
|
||||
format!("camelcase '{name}' imported as acronym '{asname}'")
|
||||
format!("Camelcase `{name}` imported as acronym `{asname}`")
|
||||
}
|
||||
// Meta
|
||||
CheckKind::UnusedNOQA(codes) => match codes {
|
||||
@@ -1258,6 +1288,22 @@ impl CheckKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// The summary text for the check. Typically a truncated form of the body text.
|
||||
pub fn summary(&self) -> String {
|
||||
match self {
|
||||
CheckKind::UnaryPrefixIncrement => {
|
||||
"Python does not support the unary prefix increment.".to_string()
|
||||
}
|
||||
CheckKind::UnusedLoopControlVariable(name) => {
|
||||
format!("Loop control variable `{name}` not used within the loop body.")
|
||||
}
|
||||
CheckKind::NoAssertRaisesException => {
|
||||
"`assertRaises(Exception):` should be considered evil.".to_string()
|
||||
}
|
||||
_ => self.body(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the check kind is (potentially) fixable.
|
||||
pub fn fixable(&self) -> bool {
|
||||
matches!(
|
||||
@@ -1293,6 +1339,7 @@ impl CheckKind {
|
||||
| CheckKind::TypeOfPrimitive(_)
|
||||
| CheckKind::UnnecessaryAbspath
|
||||
| CheckKind::UnusedImport(_)
|
||||
| CheckKind::UnusedLoopControlVariable(_)
|
||||
| CheckKind::UnusedNOQA(_)
|
||||
| CheckKind::UsePEP585Annotation(_)
|
||||
| CheckKind::UsePEP604Annotation
|
||||
|
||||
@@ -21,11 +21,14 @@ pub struct Cli {
|
||||
#[arg(long)]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Enable verbose logging.
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub verbose: bool,
|
||||
/// Disable all logging (but still exit with status code "1" upon detecting errors).
|
||||
#[arg(short, long)]
|
||||
/// Only log errors.
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub quiet: bool,
|
||||
/// Disable all logging (but still exit with status code "1" upon detecting errors).
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub silent: bool,
|
||||
/// Exit with status code "0", even upon detecting errors.
|
||||
#[arg(short, long)]
|
||||
pub exit_zero: bool,
|
||||
|
||||
42
src/cst/helpers.rs
Normal file
42
src/cst/helpers.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use libcst_native::{Expression, NameOrAttribute};
|
||||
|
||||
fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) {
|
||||
match &expr {
|
||||
Expression::Call(expr) => {
|
||||
compose_call_path_inner(&expr.func, parts);
|
||||
}
|
||||
Expression::Attribute(expr) => {
|
||||
compose_call_path_inner(&expr.value, parts);
|
||||
parts.push(expr.attr.value);
|
||||
}
|
||||
Expression::Name(expr) => {
|
||||
parts.push(expr.value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_call_path(expr: &Expression) -> Option<String> {
|
||||
let mut segments = vec![];
|
||||
compose_call_path_inner(expr, &mut segments);
|
||||
if segments.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(segments.join("."))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_module_path(module: &NameOrAttribute) -> String {
|
||||
match module {
|
||||
NameOrAttribute::N(name) => name.value.to_string(),
|
||||
NameOrAttribute::A(attr) => {
|
||||
let name = attr.attr.value;
|
||||
let prefix = compose_call_path(&attr.value);
|
||||
if let Some(prefix) = prefix {
|
||||
format!("{prefix}.{name}")
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/cst/mod.rs
Normal file
1
src/cst/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod helpers;
|
||||
@@ -25,9 +25,9 @@ pub fn leading_space(line: &str) -> String {
|
||||
}
|
||||
|
||||
/// Extract the leading indentation from a docstring.
|
||||
pub fn indentation<'a>(checker: &'a mut Checker, docstring: &Expr) -> &'a str {
|
||||
pub fn indentation<'a>(checker: &'a Checker, docstring: &Expr) -> &'a str {
|
||||
let range = Range::from_located(docstring);
|
||||
checker.locator.slice_source_code_range(&Range {
|
||||
checker.get_locator().slice_source_code_range(&Range {
|
||||
location: Location::new(range.location.row(), 1),
|
||||
end_location: Location::new(range.location.row(), range.location.column()),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
@@ -44,7 +43,7 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: &Optio
|
||||
} = &test.node
|
||||
{
|
||||
let mut check = Check::new(CheckKind::DoNotAssertFalse, Range::from_located(test));
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
let mut generator = SourceGenerator::new();
|
||||
if let Ok(()) = generator.unparse_stmt(&assertion_error(msg)) {
|
||||
if let Ok(content) = generator.generate() {
|
||||
|
||||
25
src/flake8_bugbear/plugins/assert_raises_exception.rs
Normal file
25
src/flake8_bugbear/plugins/assert_raises_exception.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use rustpython_ast::{ExprKind, Stmt, Withitem};
|
||||
|
||||
use crate::ast::helpers::match_name_or_attr;
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
/// B017
|
||||
pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[Withitem]) {
|
||||
if let Some(item) = items.first() {
|
||||
let item_context = &item.context_expr;
|
||||
if let ExprKind::Call { func, args, .. } = &item_context.node {
|
||||
if match_name_or_attr(func, "assertRaises")
|
||||
&& args.len() == 1
|
||||
&& match_name_or_attr(args.first().unwrap(), "Exception")
|
||||
&& item.optional_vars.is_none()
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoAssertRaisesException,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKi
|
||||
|
||||
use crate::ast::helpers;
|
||||
use crate::ast::types::{CheckLocator, Range};
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
@@ -50,7 +49,7 @@ pub fn duplicate_handler_exceptions(
|
||||
),
|
||||
checker.locate_check(Range::from_located(expr)),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// TODO(charlie): If we have a single element, remove the tuple.
|
||||
let mut generator = SourceGenerator::new();
|
||||
if let Ok(()) = generator.unparse_expr(&type_pattern(unique_elts), 0) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
pub use assert_false::assert_false;
|
||||
pub use assert_raises_exception::assert_raises_exception;
|
||||
pub use duplicate_exceptions::duplicate_exceptions;
|
||||
pub use duplicate_exceptions::duplicate_handler_exceptions;
|
||||
pub use unary_prefix_increment::unary_prefix_increment;
|
||||
pub use unused_loop_control_variable::unused_loop_control_variable;
|
||||
|
||||
mod assert_false;
|
||||
mod assert_raises_exception;
|
||||
mod duplicate_exceptions;
|
||||
mod unary_prefix_increment;
|
||||
mod unused_loop_control_variable;
|
||||
|
||||
19
src/flake8_bugbear/plugins/unary_prefix_increment.rs
Normal file
19
src/flake8_bugbear/plugins/unary_prefix_increment.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use rustpython_ast::{Expr, ExprKind, Unaryop};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
/// B002
|
||||
pub fn unary_prefix_increment(checker: &mut Checker, expr: &Expr, op: &Unaryop, operand: &Expr) {
|
||||
if matches!(op, Unaryop::UAdd) {
|
||||
if let ExprKind::UnaryOp { op, .. } = &operand.node {
|
||||
if matches!(op, Unaryop::UAdd) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::UnaryPrefixIncrement,
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/flake8_bugbear/plugins/unused_loop_control_variable.rs
Normal file
79
src/flake8_bugbear/plugins/unused_loop_control_variable.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rustpython_ast::{Expr, ExprKind, Stmt};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::visitor;
|
||||
use crate::ast::visitor::Visitor;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
/// Identify all `ExprKind::Name` nodes in an AST.
|
||||
struct NameFinder<'a> {
|
||||
/// A map from identifier to defining expression.
|
||||
names: BTreeMap<&'a str, &'a Expr>,
|
||||
}
|
||||
|
||||
impl NameFinder<'_> {
|
||||
fn new() -> Self {
|
||||
NameFinder {
|
||||
names: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
if let ExprKind::Name { id, .. } = &expr.node {
|
||||
self.names.insert(id, expr);
|
||||
}
|
||||
visitor::walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
/// B007
|
||||
pub fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
|
||||
let control_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
finder.visit_expr(target);
|
||||
finder.names
|
||||
};
|
||||
|
||||
let used_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
for stmt in body {
|
||||
finder.visit_stmt(stmt);
|
||||
}
|
||||
finder.names
|
||||
};
|
||||
|
||||
for (name, expr) in control_names {
|
||||
// Ignore names that are already underscore-prefixed.
|
||||
if name.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore any names that are actually used in the loop body.
|
||||
if used_names.contains_key(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut check = Check::new(
|
||||
CheckKind::UnusedLoopControlVariable(name.to_string()),
|
||||
Range::from_located(expr),
|
||||
);
|
||||
if checker.patch() {
|
||||
// Prefix the variable name with an underscore.
|
||||
check.amend(Fix::replacement(
|
||||
format!("_{name}"),
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
))
|
||||
}
|
||||
checker.add_check(check);
|
||||
}
|
||||
}
|
||||
@@ -4,57 +4,75 @@ use rustpython_ast::{Comprehension, Constant, Expr, ExprKind, KeywordData, Locat
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
fn function_name(func: &Expr) -> Option<&str> {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn exactly_one_argument_with_matching_function<'a>(
|
||||
name: &str,
|
||||
func: &Expr,
|
||||
args: &'a [Expr],
|
||||
) -> Option<&'a ExprKind> {
|
||||
if args.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
if function_name(func)? != name {
|
||||
return None;
|
||||
}
|
||||
Some(&args[0].node)
|
||||
}
|
||||
|
||||
fn first_argument_with_matching_function<'a>(
|
||||
name: &str,
|
||||
func: &Expr,
|
||||
args: &'a [Expr],
|
||||
) -> Option<&'a ExprKind> {
|
||||
if function_name(func)? != name {
|
||||
return None;
|
||||
}
|
||||
Some(&args.first()?.node)
|
||||
}
|
||||
|
||||
/// Check `list(generator)` compliance.
|
||||
pub fn unnecessary_generator_list(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "list" {
|
||||
if let ExprKind::GeneratorExp { .. } = &args[0].node {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorList,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let argument = exactly_one_argument_with_matching_function("list", func, args)?;
|
||||
if let ExprKind::GeneratorExp { .. } = argument {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorList,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check `set(generator)` compliance.
|
||||
pub fn unnecessary_generator_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "set" {
|
||||
if let ExprKind::GeneratorExp { .. } = &args[0].node {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorSet,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
|
||||
if let ExprKind::GeneratorExp { .. } = argument {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorSet,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check `dict((x, y) for x, y in iterable)` compliance.
|
||||
pub fn unnecessary_generator_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "dict" {
|
||||
if let ExprKind::GeneratorExp { elt, .. } = &args[0].node {
|
||||
match &elt.node {
|
||||
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorDict,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
|
||||
if let ExprKind::GeneratorExp { elt, .. } = argument {
|
||||
match &elt.node {
|
||||
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorDict,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -66,17 +84,12 @@ pub fn unnecessary_list_comprehension_set(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "set" {
|
||||
if let ExprKind::ListComp { .. } = &args[0].node {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListComprehensionSet,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
|
||||
if let ExprKind::ListComp { .. } = &argument {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListComprehensionSet,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -87,21 +100,16 @@ pub fn unnecessary_list_comprehension_dict(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "dict" {
|
||||
if let ExprKind::ListComp { elt, .. } = &args[0].node {
|
||||
match &elt.node {
|
||||
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListComprehensionDict,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
|
||||
if let ExprKind::ListComp { elt, .. } = &argument {
|
||||
match &elt.node {
|
||||
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListComprehensionDict,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -109,82 +117,38 @@ pub fn unnecessary_list_comprehension_dict(
|
||||
|
||||
/// Check `set([1, 2])` compliance.
|
||||
pub fn unnecessary_literal_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "set" {
|
||||
match &args[0].node {
|
||||
ExprKind::List { .. } => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralSet("list".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
ExprKind::Tuple { .. } => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralSet("tuple".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
|
||||
let kind = match argument {
|
||||
ExprKind::List { .. } => "list",
|
||||
ExprKind::Tuple { .. } => "tuple",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralSet(kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
/// Check `dict([(1, 2)])` compliance.
|
||||
pub fn unnecessary_literal_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "dict" {
|
||||
match &args[0].node {
|
||||
ExprKind::Tuple { elts, .. } => {
|
||||
if let Some(elt) = elts.first() {
|
||||
match &elt.node {
|
||||
// dict((1, 2), ...))
|
||||
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralDict("tuple".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// dict(())
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralDict("tuple".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
ExprKind::List { elts, .. } => {
|
||||
if let Some(elt) = elts.first() {
|
||||
match &elt.node {
|
||||
// dict([(1, 2), ...])
|
||||
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralDict("list".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// dict([])
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralDict("list".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
|
||||
let (kind, elts) = match argument {
|
||||
ExprKind::Tuple { elts, .. } => ("tuple", elts),
|
||||
ExprKind::List { elts, .. } => ("list", elts),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if let Some(elt) = elts.first() {
|
||||
// dict((1, 2), ...)) or dict([(1, 2), ...])
|
||||
if !matches!(&elt.node, ExprKind::Tuple { elts, .. } if elts.len() == 2) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralDict(kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn unnecessary_collection_call(
|
||||
@@ -193,26 +157,21 @@ pub fn unnecessary_collection_call(
|
||||
args: &[Expr],
|
||||
keywords: &[Located<KeywordData>],
|
||||
) -> Option<Check> {
|
||||
if args.is_empty() {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "list" || id == "tuple" {
|
||||
// list() or tuple()
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryCollectionCall(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
} else if id == "dict" {
|
||||
// dict() or dict(a=1)
|
||||
if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryCollectionCall(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !args.is_empty() {
|
||||
return None;
|
||||
}
|
||||
None
|
||||
let id = function_name(func)?;
|
||||
match id {
|
||||
"list" | "tuple" => {
|
||||
// list() or tuple()
|
||||
}
|
||||
"dict" if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) => (),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryCollectionCall(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn unnecessary_literal_within_tuple_call(
|
||||
@@ -220,29 +179,16 @@ pub fn unnecessary_literal_within_tuple_call(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "tuple" {
|
||||
if let Some(arg) = args.first() {
|
||||
match &arg.node {
|
||||
ExprKind::Tuple { .. } => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall("tuple".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
ExprKind::List { .. } => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall("list".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
let argument = first_argument_with_matching_function("tuple", func, args)?;
|
||||
let argument_kind = match argument {
|
||||
ExprKind::Tuple { .. } => "tuple",
|
||||
ExprKind::List { .. } => "list",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall(argument_kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn unnecessary_literal_within_list_call(
|
||||
@@ -250,62 +196,40 @@ pub fn unnecessary_literal_within_list_call(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "list" {
|
||||
if let Some(arg) = args.first() {
|
||||
match &arg.node {
|
||||
ExprKind::Tuple { .. } => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralWithinListCall("tuple".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
ExprKind::List { .. } => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralWithinListCall("list".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
let argument = first_argument_with_matching_function("list", func, args)?;
|
||||
let argument_kind = match argument {
|
||||
ExprKind::Tuple { .. } => "tuple",
|
||||
ExprKind::List { .. } => "list",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralWithinListCall(argument_kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn unnecessary_list_call(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "list" {
|
||||
if let Some(arg) = args.first() {
|
||||
if let ExprKind::ListComp { .. } = &arg.node {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListCall,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let argument = first_argument_with_matching_function("list", func, args)?;
|
||||
if let ExprKind::ListComp { .. } = argument {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListCall,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn unnecessary_call_around_sorted(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if let ExprKind::Name { id: outer, .. } = &func.node {
|
||||
if outer == "list" || outer == "reversed" {
|
||||
if let Some(arg) = args.first() {
|
||||
if let ExprKind::Call { func, .. } = &arg.node {
|
||||
if let ExprKind::Name { id: inner, .. } = &func.node {
|
||||
if inner == "sorted" {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryCallAroundSorted(outer.to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let outer = function_name(func)?;
|
||||
if !(outer == "list" || outer == "reversed") {
|
||||
return None;
|
||||
}
|
||||
if let ExprKind::Call { func, .. } = &args.first()?.node {
|
||||
if function_name(func)? == "sorted" {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryCallAroundSorted(outer.to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -316,91 +240,65 @@ pub fn unnecessary_double_cast_or_process(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
if let ExprKind::Name { id: outer, .. } = &func.node {
|
||||
if outer == "list"
|
||||
|| outer == "tuple"
|
||||
|| outer == "set"
|
||||
|| outer == "reversed"
|
||||
|| outer == "sorted"
|
||||
let outer = function_name(func)?;
|
||||
if !["list", "tuple", "set", "reversed", "sorted"].contains(&outer) {
|
||||
return None;
|
||||
}
|
||||
|
||||
fn new_check(inner: &str, outer: &str, expr: &Expr) -> Check {
|
||||
Check::new(
|
||||
CheckKind::UnnecessaryDoubleCastOrProcess(inner.to_string(), outer.to_string()),
|
||||
Range::from_located(expr),
|
||||
)
|
||||
}
|
||||
|
||||
if let ExprKind::Call { func, .. } = &args.first()?.node {
|
||||
let inner = function_name(func)?;
|
||||
// Ex) set(tuple(...))
|
||||
if (outer == "set" || outer == "sorted")
|
||||
&& (inner == "list" || inner == "tuple" || inner == "reversed" || inner == "sorted")
|
||||
{
|
||||
if let Some(arg) = args.first() {
|
||||
if let ExprKind::Call { func, .. } = &arg.node {
|
||||
if let ExprKind::Name { id: inner, .. } = &func.node {
|
||||
// Ex) set(tuple(...))
|
||||
if (outer == "set" || outer == "sorted")
|
||||
&& (inner == "list"
|
||||
|| inner == "tuple"
|
||||
|| inner == "reversed"
|
||||
|| inner == "sorted")
|
||||
{
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryDoubleCastOrProcess(
|
||||
inner.to_string(),
|
||||
outer.to_string(),
|
||||
),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
return Some(new_check(inner, outer, expr));
|
||||
}
|
||||
|
||||
// Ex) list(tuple(...))
|
||||
if (outer == "list" || outer == "tuple")
|
||||
&& (inner == "list" || inner == "tuple")
|
||||
{
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryDoubleCastOrProcess(
|
||||
inner.to_string(),
|
||||
outer.to_string(),
|
||||
),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
// Ex) list(tuple(...))
|
||||
if (outer == "list" || outer == "tuple") && (inner == "list" || inner == "tuple") {
|
||||
return Some(new_check(inner, outer, expr));
|
||||
}
|
||||
|
||||
// Ex) set(set(...))
|
||||
if outer == "set" && inner == "set" {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryDoubleCastOrProcess(
|
||||
inner.to_string(),
|
||||
outer.to_string(),
|
||||
),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ex) set(set(...))
|
||||
if outer == "set" && inner == "set" {
|
||||
return Some(new_check(inner, outer, expr));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if let Some(first_arg) = args.first() {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "set" || id == "sorted" || id == "reversed" {
|
||||
if let ExprKind::Subscript { slice, .. } = &first_arg.node {
|
||||
if let ExprKind::Slice { lower, upper, step } = &slice.node {
|
||||
if lower.is_none() && upper.is_none() {
|
||||
if let Some(step) = step {
|
||||
if let ExprKind::UnaryOp {
|
||||
op: Unaryop::USub,
|
||||
operand,
|
||||
} = &step.node
|
||||
{
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Int(val),
|
||||
..
|
||||
} = &operand.node
|
||||
{
|
||||
if *val == BigInt::from(1) {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessarySubscriptReversal(
|
||||
id.to_string(),
|
||||
),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let first_arg = args.first()?;
|
||||
let id = function_name(func)?;
|
||||
if !["set", "sorted", "reversed"].contains(&id) {
|
||||
return None;
|
||||
}
|
||||
if let ExprKind::Subscript { slice, .. } = &first_arg.node {
|
||||
if let ExprKind::Slice { lower, upper, step } = &slice.node {
|
||||
if lower.is_none() && upper.is_none() {
|
||||
if let Some(step) = step {
|
||||
if let ExprKind::UnaryOp {
|
||||
op: Unaryop::USub,
|
||||
operand,
|
||||
} = &step.node
|
||||
{
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Int(val),
|
||||
..
|
||||
} = &operand.node
|
||||
{
|
||||
if *val == BigInt::from(1) {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessarySubscriptReversal(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,90 +314,65 @@ pub fn unnecessary_comprehension(
|
||||
elt: &Expr,
|
||||
generators: &[Comprehension],
|
||||
) -> Option<Check> {
|
||||
if generators.len() == 1 {
|
||||
let generator = &generators[0];
|
||||
if generator.ifs.is_empty() && generator.is_async == 0 {
|
||||
if let ExprKind::Name { id: elt_id, .. } = &elt.node {
|
||||
if let ExprKind::Name { id: target_id, .. } = &generator.target.node {
|
||||
if elt_id == target_id {
|
||||
match &expr.node {
|
||||
ExprKind::ListComp { .. } => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryComprehension("list".to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
ExprKind::SetComp { .. } => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryComprehension("set".to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if generators.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
None
|
||||
let generator = &generators[0];
|
||||
if !(generator.ifs.is_empty() && generator.is_async == 0) {
|
||||
return None;
|
||||
}
|
||||
let elt_id = function_name(elt)?;
|
||||
let target_id = function_name(&generator.target)?;
|
||||
if elt_id != target_id {
|
||||
return None;
|
||||
}
|
||||
let expr_kind = match &expr.node {
|
||||
ExprKind::ListComp { .. } => "list",
|
||||
ExprKind::SetComp { .. } => "set",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryComprehension(expr_kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn unnecessary_map(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "map" {
|
||||
if args.len() == 2 {
|
||||
if let ExprKind::Lambda { .. } = &args[0].node {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryMap("generator".to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
fn new_check(kind: &str, expr: &Expr) -> Check {
|
||||
Check::new(
|
||||
CheckKind::UnnecessaryMap(kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
)
|
||||
}
|
||||
let id = function_name(func)?;
|
||||
match id {
|
||||
"map" => {
|
||||
if args.len() == 2 && matches!(&args[0].node, ExprKind::Lambda { .. }) {
|
||||
return Some(new_check("generator", expr));
|
||||
}
|
||||
}
|
||||
"list" | "set" => {
|
||||
if let ExprKind::Call { func, args, .. } = &args.first()?.node {
|
||||
let argument = first_argument_with_matching_function("map", func, args)?;
|
||||
if let ExprKind::Lambda { .. } = argument {
|
||||
return Some(new_check(id, expr));
|
||||
}
|
||||
}
|
||||
} else if id == "list" || id == "set" {
|
||||
if let Some(arg) = args.first() {
|
||||
if let ExprKind::Call { func, args, .. } = &arg.node {
|
||||
if let ExprKind::Name { id: f, .. } = &func.node {
|
||||
if f == "map" {
|
||||
if let Some(arg) = args.first() {
|
||||
if let ExprKind::Lambda { .. } = &arg.node {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryMap(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if id == "dict" {
|
||||
}
|
||||
"dict" => {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Call { func, args, .. } = &args[0].node {
|
||||
if let ExprKind::Name { id: f, .. } = &func.node {
|
||||
if f == "map" {
|
||||
if let Some(arg) = args.first() {
|
||||
if let ExprKind::Lambda { body, .. } = &arg.node {
|
||||
match &body.node {
|
||||
ExprKind::Tuple { elts, .. }
|
||||
| ExprKind::List { elts, .. }
|
||||
if elts.len() == 2 =>
|
||||
{
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryMap(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let argument = first_argument_with_matching_function("map", func, args)?;
|
||||
if let ExprKind::Lambda { body, .. } = &argument {
|
||||
if matches!(&body.node, ExprKind::Tuple { elts, .. } | ExprKind::List { elts, .. } if elts.len() == 2)
|
||||
{
|
||||
return Some(new_check(id, expr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use log::error;
|
||||
use rustpython_ast::{Expr, Stmt, StmtKind};
|
||||
|
||||
use crate::autofix::{fixer, helpers};
|
||||
use crate::autofix::helpers;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::CheckCode;
|
||||
use crate::flake8_print::checks;
|
||||
@@ -13,7 +13,7 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
|
||||
checker.settings.enabled.contains(&CheckCode::T201),
|
||||
checker.settings.enabled.contains(&CheckCode::T203),
|
||||
) {
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
let context = checker.binding_context();
|
||||
if matches!(
|
||||
checker.parents[context.defined_by].node,
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@@ -16,9 +16,11 @@ mod autofix;
|
||||
pub mod cache;
|
||||
pub mod check_ast;
|
||||
mod check_lines;
|
||||
mod check_tokens;
|
||||
pub mod checks;
|
||||
pub mod cli;
|
||||
pub mod code_gen;
|
||||
mod cst;
|
||||
mod docstrings;
|
||||
mod flake8_bugbear;
|
||||
mod flake8_builtins;
|
||||
@@ -41,7 +43,7 @@ pub mod settings;
|
||||
pub mod visibility;
|
||||
|
||||
/// Run ruff over Python source code directly.
|
||||
pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
|
||||
pub fn check(path: &Path, contents: &str, quiet: bool) -> Result<Vec<Message>> {
|
||||
// Find the project root and pyproject.toml.
|
||||
let project_root = pyproject::find_project_root(&[path.to_path_buf()]);
|
||||
match &project_root {
|
||||
@@ -54,7 +56,11 @@ pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
|
||||
None => debug!("Unable to find pyproject.toml; using default settings..."),
|
||||
};
|
||||
|
||||
let settings = Settings::from_raw(RawSettings::from_pyproject(&pyproject, &project_root)?);
|
||||
let settings = Settings::from_raw(RawSettings::from_pyproject(
|
||||
&pyproject,
|
||||
&project_root,
|
||||
quiet,
|
||||
)?);
|
||||
|
||||
// Tokenize once.
|
||||
let tokens: Vec<LexResult> = tokenize(contents);
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::autofix::fixer;
|
||||
use crate::autofix::fixer::fix_file;
|
||||
use crate::check_ast::check_ast;
|
||||
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::message::Message;
|
||||
@@ -45,6 +46,15 @@ pub(crate) fn check_path(
|
||||
// Aggregate all checks.
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
// Run the token-based checks.
|
||||
if settings
|
||||
.enabled
|
||||
.iter()
|
||||
.any(|check_code| matches!(check_code.lint_source(), LintSource::Tokens))
|
||||
{
|
||||
check_tokens(&mut checks, contents, &tokens, settings);
|
||||
}
|
||||
|
||||
// Run the AST-based checks.
|
||||
if settings
|
||||
.enabled
|
||||
@@ -245,8 +255,11 @@ mod tests {
|
||||
#[test_case(CheckCode::A001, Path::new("A001.py"); "A001")]
|
||||
#[test_case(CheckCode::A002, Path::new("A002.py"); "A002")]
|
||||
#[test_case(CheckCode::A003, Path::new("A003.py"); "A003")]
|
||||
#[test_case(CheckCode::B002, Path::new("B002.py"); "B002")]
|
||||
#[test_case(CheckCode::B007, Path::new("B007.py"); "B007")]
|
||||
#[test_case(CheckCode::B011, Path::new("B011.py"); "B011")]
|
||||
#[test_case(CheckCode::B014, Path::new("B014.py"); "B014")]
|
||||
#[test_case(CheckCode::B017, Path::new("B017.py"); "B017")]
|
||||
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
|
||||
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
|
||||
#[test_case(CheckCode::C401, Path::new("C401.py"); "C401")]
|
||||
@@ -327,6 +340,7 @@ mod tests {
|
||||
#[test_case(CheckCode::F401, Path::new("F401_2.py"); "F401_2")]
|
||||
#[test_case(CheckCode::F401, Path::new("F401_3.py"); "F401_3")]
|
||||
#[test_case(CheckCode::F401, Path::new("F401_4.py"); "F401_4")]
|
||||
#[test_case(CheckCode::F401, Path::new("F401_5.py"); "F401_5")]
|
||||
#[test_case(CheckCode::F402, Path::new("F402.py"); "F402")]
|
||||
#[test_case(CheckCode::F403, Path::new("F403.py"); "F403")]
|
||||
#[test_case(CheckCode::F404, Path::new("F404.py"); "F404")]
|
||||
@@ -377,6 +391,7 @@ mod tests {
|
||||
#[test_case(CheckCode::W292, Path::new("W292_0.py"); "W292_0")]
|
||||
#[test_case(CheckCode::W292, Path::new("W292_1.py"); "W292_1")]
|
||||
#[test_case(CheckCode::W292, Path::new("W292_2.py"); "W292_2")]
|
||||
#[test_case(CheckCode::W605, Path::new("W605.py"); "W605")]
|
||||
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
|
||||
13
src/main.rs
13
src/main.rs
@@ -220,7 +220,8 @@ fn autoformat(files: &[PathBuf], settings: &Settings) -> Result<usize> {
|
||||
}
|
||||
|
||||
fn inner_main() -> Result<ExitCode> {
|
||||
let cli = Cli::parse();
|
||||
let mut cli = Cli::parse();
|
||||
cli.quiet |= cli.silent;
|
||||
|
||||
set_up_logging(cli.verbose)?;
|
||||
|
||||
@@ -255,7 +256,7 @@ fn inner_main() -> Result<ExitCode> {
|
||||
.map(|pair| PerFileIgnore::new(pair, &project_root))
|
||||
.collect();
|
||||
|
||||
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root)?;
|
||||
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root, cli.quiet)?;
|
||||
if !exclude.is_empty() {
|
||||
settings.exclude = exclude;
|
||||
}
|
||||
@@ -342,7 +343,7 @@ fn inner_main() -> Result<ExitCode> {
|
||||
tell_user!("Starting linter in watch mode...\n");
|
||||
|
||||
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
|
||||
if !cli.quiet {
|
||||
if !cli.silent {
|
||||
printer.write_continuously(&messages)?;
|
||||
}
|
||||
|
||||
@@ -362,7 +363,7 @@ fn inner_main() -> Result<ExitCode> {
|
||||
tell_user!("File change detected...\n");
|
||||
|
||||
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
|
||||
if !cli.quiet {
|
||||
if !cli.silent {
|
||||
printer.write_continuously(&messages)?;
|
||||
}
|
||||
}
|
||||
@@ -388,13 +389,13 @@ fn inner_main() -> Result<ExitCode> {
|
||||
let path = Path::new(&filename);
|
||||
(
|
||||
run_once_stdin(&settings, path, cli.fix)?,
|
||||
!cli.quiet && !cli.fix,
|
||||
!cli.silent && !cli.fix,
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?,
|
||||
!cli.quiet,
|
||||
!cli.silent,
|
||||
!cli.quiet,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -114,12 +114,36 @@ pub fn dunder_function_name(func_def: &Stmt, scope: &Scope, name: &str) -> Optio
|
||||
None
|
||||
}
|
||||
|
||||
fn is_lower(s: &str) -> bool {
|
||||
let mut cased = false;
|
||||
for c in s.chars() {
|
||||
if c.is_uppercase() {
|
||||
return false;
|
||||
} else if !cased && c.is_lowercase() {
|
||||
cased = true;
|
||||
}
|
||||
}
|
||||
cased
|
||||
}
|
||||
|
||||
fn is_upper(s: &str) -> bool {
|
||||
let mut cased = false;
|
||||
for c in s.chars() {
|
||||
if c.is_lowercase() {
|
||||
return false;
|
||||
} else if (!cased) && c.is_uppercase() {
|
||||
cased = true;
|
||||
}
|
||||
}
|
||||
cased
|
||||
}
|
||||
|
||||
pub fn constant_imported_as_non_constant(
|
||||
import_from: &Stmt,
|
||||
name: &str,
|
||||
asname: &str,
|
||||
) -> Option<Check> {
|
||||
if name.chars().all(|c| c.is_uppercase()) && !asname.chars().all(|c| c.is_uppercase()) {
|
||||
if is_upper(name) && !is_upper(asname) {
|
||||
return Some(Check::new(
|
||||
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
@@ -133,7 +157,7 @@ pub fn lowercase_imported_as_non_lowercase(
|
||||
name: &str,
|
||||
asname: &str,
|
||||
) -> Option<Check> {
|
||||
if name.chars().all(|c| c.is_lowercase()) && asname.to_lowercase() != asname {
|
||||
if is_lower(name) && asname.to_lowercase() != asname {
|
||||
return Some(Check::new(
|
||||
CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
@@ -143,9 +167,8 @@ pub fn lowercase_imported_as_non_lowercase(
|
||||
}
|
||||
|
||||
fn is_camelcase(name: &str) -> bool {
|
||||
!name.chars().all(|c| c.is_uppercase()) && !name.chars().all(|c| c.is_lowercase())
|
||||
!is_lower(name) && !is_upper(name) && !name.contains('_')
|
||||
}
|
||||
|
||||
fn is_acronym(name: &str, asname: &str) -> bool {
|
||||
name.chars().filter(|c| c.is_uppercase()).join("") == asname
|
||||
}
|
||||
@@ -155,7 +178,7 @@ pub fn camelcase_imported_as_lowercase(
|
||||
name: &str,
|
||||
asname: &str,
|
||||
) -> Option<Check> {
|
||||
if is_camelcase(name) && asname.chars().all(|c| c.is_lowercase()) {
|
||||
if is_camelcase(name) && is_lower(asname) {
|
||||
return Some(Check::new(
|
||||
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
@@ -169,7 +192,7 @@ pub fn camelcase_imported_as_constant(
|
||||
name: &str,
|
||||
asname: &str,
|
||||
) -> Option<Check> {
|
||||
if is_camelcase(name) && asname.chars().all(|c| c.is_uppercase()) && !is_acronym(name, asname) {
|
||||
if is_camelcase(name) && is_upper(asname) && !is_acronym(name, asname) {
|
||||
return Some(Check::new(
|
||||
CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
@@ -183,7 +206,7 @@ pub fn camelcase_imported_as_acronym(
|
||||
name: &str,
|
||||
asname: &str,
|
||||
) -> Option<Check> {
|
||||
if is_camelcase(name) && asname.chars().all(|c| c.is_uppercase()) && is_acronym(name, asname) {
|
||||
if is_camelcase(name) && is_upper(asname) && is_acronym(name, asname) {
|
||||
return Some(Check::new(
|
||||
CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
@@ -191,3 +214,48 @@ pub fn camelcase_imported_as_acronym(
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{is_acronym, is_camelcase, is_lower, is_upper};
|
||||
|
||||
#[test]
|
||||
fn test_is_lower() -> () {
|
||||
assert!(is_lower("abc"));
|
||||
assert!(is_lower("a_b_c"));
|
||||
assert!(is_lower("a2c"));
|
||||
assert!(!is_lower("aBc"));
|
||||
assert!(!is_lower("ABC"));
|
||||
assert!(!is_lower(""));
|
||||
assert!(!is_lower("_"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_upper() -> () {
|
||||
assert!(is_upper("ABC"));
|
||||
assert!(is_upper("A_B_C"));
|
||||
assert!(is_upper("A2C"));
|
||||
assert!(!is_upper("aBc"));
|
||||
assert!(!is_upper("abc"));
|
||||
assert!(!is_upper(""));
|
||||
assert!(!is_upper("_"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_camelcase() -> () {
|
||||
assert!(is_camelcase("Camel"));
|
||||
assert!(is_camelcase("CamelCase"));
|
||||
assert!(!is_camelcase("camel"));
|
||||
assert!(!is_camelcase("camel_case"));
|
||||
assert!(!is_camelcase("CAMEL"));
|
||||
assert!(!is_camelcase("CAMEL_CASE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_acronym() -> () {
|
||||
assert!(is_acronym("AB", "AB"));
|
||||
assert!(is_acronym("AbcDef", "AD"));
|
||||
assert!(!is_acronym("AbcDef", "Ad"));
|
||||
assert!(!is_acronym("AbcDef", "AB"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use regex::Regex;
|
||||
use rustpython_ast::{Arg, Constant, ExprKind, Location, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
@@ -162,12 +161,12 @@ pub fn blank_before_after_function(checker: &mut Checker, definition: &Definitio
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let (before, _, after) = checker.locator.partition_source_code_at(
|
||||
&Range::from_located(parent),
|
||||
&Range::from_located(docstring),
|
||||
);
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D201) {
|
||||
let (before, _, _) = checker.get_locator().partition_source_code_at(
|
||||
&Range::from_located(parent),
|
||||
&Range::from_located(docstring),
|
||||
);
|
||||
|
||||
let blank_lines_before = before
|
||||
.lines()
|
||||
.rev()
|
||||
@@ -179,7 +178,7 @@ pub fn blank_before_after_function(checker: &mut Checker, definition: &Definitio
|
||||
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Delete the blank line before the docstring.
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(docstring.location.row() - blank_lines_before, 1),
|
||||
@@ -191,6 +190,11 @@ pub fn blank_before_after_function(checker: &mut Checker, definition: &Definitio
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D202) {
|
||||
let (_, _, after) = checker.get_locator().partition_source_code_at(
|
||||
&Range::from_located(parent),
|
||||
&Range::from_located(docstring),
|
||||
);
|
||||
|
||||
let all_blank_after = after
|
||||
.lines()
|
||||
.skip(1)
|
||||
@@ -217,7 +221,7 @@ pub fn blank_before_after_function(checker: &mut Checker, definition: &Definitio
|
||||
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Delete the blank line after the docstring.
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(
|
||||
@@ -246,14 +250,14 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let (before, _, after) = checker.locator.partition_source_code_at(
|
||||
&Range::from_located(parent),
|
||||
&Range::from_located(docstring),
|
||||
);
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D203)
|
||||
|| checker.settings.enabled.contains(&CheckCode::D211)
|
||||
{
|
||||
let (before, _, _) = checker.get_locator().partition_source_code_at(
|
||||
&Range::from_located(parent),
|
||||
&Range::from_located(docstring),
|
||||
);
|
||||
|
||||
let blank_lines_before = before
|
||||
.lines()
|
||||
.rev()
|
||||
@@ -266,8 +270,7 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
|
||||
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
|
||||
{
|
||||
if checker.patch() {
|
||||
// Delete the blank line before the class.
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(docstring.location.row() - blank_lines_before, 1),
|
||||
@@ -283,8 +286,7 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
|
||||
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
|
||||
{
|
||||
if checker.patch() {
|
||||
// Insert one blank line before the class.
|
||||
check.amend(Fix::replacement(
|
||||
"\n".to_string(),
|
||||
@@ -298,6 +300,11 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D204) {
|
||||
let (_, _, after) = checker.get_locator().partition_source_code_at(
|
||||
&Range::from_located(parent),
|
||||
&Range::from_located(docstring),
|
||||
);
|
||||
|
||||
let all_blank_after = after
|
||||
.lines()
|
||||
.skip(1)
|
||||
@@ -316,7 +323,7 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
|
||||
CheckKind::OneBlankLineAfterClass(blank_lines_after),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Insert a blank line before the class (replacing any existing lines).
|
||||
check.amend(Fix::replacement(
|
||||
"\n".to_string(),
|
||||
@@ -358,7 +365,7 @@ pub fn blank_after_summary(checker: &mut Checker, definition: &Definition) {
|
||||
CheckKind::BlankLineAfterSummary,
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Insert one blank line after the summary (replacing any existing lines).
|
||||
check.amend(Fix::replacement(
|
||||
"\n".to_string(),
|
||||
@@ -417,7 +424,7 @@ pub fn indent(checker: &mut Checker, definition: &Definition) {
|
||||
end_location: Location::new(docstring.location.row() + i, 1),
|
||||
},
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
check.amend(Fix::replacement(
|
||||
helpers::clean(&docstring_indent),
|
||||
Location::new(docstring.location.row() + i, 1),
|
||||
@@ -466,8 +473,7 @@ pub fn indent(checker: &mut Checker, definition: &Definition) {
|
||||
end_location: Location::new(docstring.location.row() + i, 1),
|
||||
},
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
|
||||
{
|
||||
if checker.patch() {
|
||||
check.amend(Fix::replacement(
|
||||
helpers::clean(&docstring_indent),
|
||||
Location::new(docstring.location.row() + i, 1),
|
||||
@@ -494,7 +500,7 @@ pub fn indent(checker: &mut Checker, definition: &Definition) {
|
||||
end_location: Location::new(docstring.location.row() + i, 1),
|
||||
},
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
check.amend(Fix::replacement(
|
||||
helpers::clean(&docstring_indent),
|
||||
Location::new(docstring.location.row() + i, 1),
|
||||
@@ -524,7 +530,7 @@ pub fn newline_after_last_paragraph(checker: &mut Checker, definition: &Definiti
|
||||
}
|
||||
if line_count > 1 {
|
||||
let content = checker
|
||||
.locator
|
||||
.get_locator()
|
||||
.slice_source_code_range(&Range::from_located(docstring));
|
||||
if let Some(last_line) = content.lines().last().map(|line| line.trim()) {
|
||||
if last_line != "\"\"\"" && last_line != "'''" {
|
||||
@@ -532,8 +538,7 @@ pub fn newline_after_last_paragraph(checker: &mut Checker, definition: &Definiti
|
||||
CheckKind::NewLineAfterLastParagraph,
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
|
||||
{
|
||||
if checker.patch() {
|
||||
// Insert a newline just before the end-quote(s).
|
||||
let content = format!(
|
||||
"\n{}",
|
||||
@@ -576,9 +581,9 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, definition: &Definition)
|
||||
CheckKind::NoSurroundingWhitespace,
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
if let Some(first_line) = checker
|
||||
.locator
|
||||
.get_locator()
|
||||
.slice_source_code_range(&Range::from_located(docstring))
|
||||
.lines()
|
||||
.next()
|
||||
@@ -624,7 +629,7 @@ pub fn multi_line_summary_start(checker: &mut Checker, definition: &Definition)
|
||||
{
|
||||
if string.lines().nth(1).is_some() {
|
||||
if let Some(first_line) = checker
|
||||
.locator
|
||||
.get_locator()
|
||||
.slice_source_code_range(&Range::from_located(docstring))
|
||||
.lines()
|
||||
.next()
|
||||
@@ -660,7 +665,7 @@ pub fn triple_quotes(checker: &mut Checker, definition: &Definition) {
|
||||
} = &docstring.node
|
||||
{
|
||||
if let Some(first_line) = checker
|
||||
.locator
|
||||
.get_locator()
|
||||
.slice_source_code_range(&Range::from_located(docstring))
|
||||
.lines()
|
||||
.next()
|
||||
@@ -911,7 +916,7 @@ fn blanks_and_section_underline(
|
||||
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Add a dashed line (of the appropriate length) under the section header.
|
||||
let content = format!(
|
||||
"{}{}\n",
|
||||
@@ -945,7 +950,7 @@ fn blanks_and_section_underline(
|
||||
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Add a dashed line (of the appropriate length) under the section header.
|
||||
let content = format!(
|
||||
"{}{}\n",
|
||||
@@ -967,7 +972,7 @@ fn blanks_and_section_underline(
|
||||
),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Delete any blank lines between the header and content.
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(docstring.location.row() + context.original_index + 1, 1),
|
||||
@@ -990,7 +995,7 @@ fn blanks_and_section_underline(
|
||||
CheckKind::SectionUnderlineAfterName(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Delete any blank lines between the header and the underline.
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(docstring.location.row() + context.original_index + 1, 1),
|
||||
@@ -1021,7 +1026,7 @@ fn blanks_and_section_underline(
|
||||
),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Replace the existing underline with a line of the appropriate length.
|
||||
let content = format!(
|
||||
"{}{}\n",
|
||||
@@ -1059,7 +1064,7 @@ fn blanks_and_section_underline(
|
||||
CheckKind::SectionUnderlineNotOverIndented(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Replace the existing indentation with whitespace of the appropriate length.
|
||||
check.amend(Fix::replacement(
|
||||
helpers::clean(&indentation),
|
||||
@@ -1108,7 +1113,7 @@ fn blanks_and_section_underline(
|
||||
),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Delete any blank lines between the header and content.
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(
|
||||
@@ -1167,7 +1172,7 @@ fn common_section(
|
||||
CheckKind::CapitalizeSectionName(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Replace the section title with the capitalized variant. This requires
|
||||
// locating the start and end of the section name.
|
||||
if let Some(index) = context.line.find(&context.section_name) {
|
||||
@@ -1200,7 +1205,7 @@ fn common_section(
|
||||
CheckKind::SectionNotOverIndented(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Replace the existing indentation with whitespace of the appropriate length.
|
||||
check.amend(Fix::replacement(
|
||||
helpers::clean(&indentation),
|
||||
@@ -1227,7 +1232,7 @@ fn common_section(
|
||||
CheckKind::BlankLineAfterLastSection(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Add a newline after the section.
|
||||
check.amend(Fix::insertion(
|
||||
"\n".to_string(),
|
||||
@@ -1248,7 +1253,7 @@ fn common_section(
|
||||
CheckKind::BlankLineAfterSection(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Add a newline after the section.
|
||||
check.amend(Fix::insertion(
|
||||
"\n".to_string(),
|
||||
@@ -1272,7 +1277,7 @@ fn common_section(
|
||||
CheckKind::BlankLineBeforeSection(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Add a blank line before the section.
|
||||
check.amend(Fix::insertion(
|
||||
"\n".to_string(),
|
||||
@@ -1432,7 +1437,7 @@ fn numpy_section(checker: &mut Checker, definition: &Definition, context: &Secti
|
||||
CheckKind::NewLineAfterSectionName(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Delete the suffix. This requires locating the end of the section name.
|
||||
if let Some(index) = context.line.find(&context.section_name) {
|
||||
// Map from bytes to characters.
|
||||
@@ -1481,7 +1486,7 @@ fn google_section(checker: &mut Checker, definition: &Definition, context: &Sect
|
||||
CheckKind::SectionNameEndsInColon(context.section_name.to_string()),
|
||||
Range::from_located(docstring),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// Replace the suffix. This requires locating the end of the section name.
|
||||
if let Some(index) = context.line.find(&context.section_name) {
|
||||
// Map from bytes to characters.
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
use libcst_native::ImportNames::Aliases;
|
||||
use libcst_native::NameOrAttribute::N;
|
||||
use libcst_native::{Codegen, SmallStatement, Statement};
|
||||
use anyhow::Result;
|
||||
use libcst_native::{Codegen, ImportNames, NameOrAttribute, SmallStatement, Statement};
|
||||
use rustpython_ast::Stmt;
|
||||
|
||||
use crate::ast::operations::SourceCodeLocator;
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::{helpers, Fix};
|
||||
use crate::cst::helpers::compose_module_path;
|
||||
|
||||
/// Generate a Fix to remove any unused imports from an `import` statement.
|
||||
pub fn remove_unused_imports(
|
||||
locator: &mut SourceCodeLocator,
|
||||
locator: &SourceCodeLocator,
|
||||
full_names: &[&str],
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
) -> anyhow::Result<Fix> {
|
||||
) -> Result<Fix> {
|
||||
let mut tree = match libcst_native::parse_module(
|
||||
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||
None,
|
||||
@@ -40,13 +40,11 @@ pub fn remove_unused_imports(
|
||||
// Preserve the trailing comma (or not) from the last entry.
|
||||
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
||||
|
||||
// Identify unused imports from within the `import from`.
|
||||
// Identify unused imports from within the `import`.
|
||||
let mut removable = vec![];
|
||||
for (index, alias) in aliases.iter().enumerate() {
|
||||
if let N(import_name) = &alias.name {
|
||||
if full_names.contains(&import_name.value) {
|
||||
removable.push(index);
|
||||
}
|
||||
if full_names.contains(&compose_module_path(&alias.name).as_str()) {
|
||||
removable.push(index);
|
||||
}
|
||||
}
|
||||
// TODO(charlie): This is quadratic.
|
||||
@@ -74,12 +72,12 @@ pub fn remove_unused_imports(
|
||||
|
||||
/// Generate a Fix to remove any unused imports from an `import from` statement.
|
||||
pub fn remove_unused_import_froms(
|
||||
locator: &mut SourceCodeLocator,
|
||||
locator: &SourceCodeLocator,
|
||||
full_names: &[&str],
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
) -> anyhow::Result<Fix> {
|
||||
) -> Result<Fix> {
|
||||
let mut tree = match libcst_native::parse_module(
|
||||
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||
None,
|
||||
@@ -100,7 +98,8 @@ pub fn remove_unused_import_froms(
|
||||
"Expected node to be: SmallStatement::ImportFrom."
|
||||
));
|
||||
};
|
||||
let aliases = if let Aliases(aliases) = &mut body.names {
|
||||
|
||||
let aliases = if let ImportNames::Aliases(aliases) = &mut body.names {
|
||||
aliases
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Expected node to be: Aliases."));
|
||||
@@ -112,13 +111,16 @@ pub fn remove_unused_import_froms(
|
||||
// Identify unused imports from within the `import from`.
|
||||
let mut removable = vec![];
|
||||
for (index, alias) in aliases.iter().enumerate() {
|
||||
if let N(name) = &alias.name {
|
||||
let import_name = if let Some(N(module_name)) = &body.module {
|
||||
format!("{}.{}", module_name.value, name.value)
|
||||
} else {
|
||||
name.value.to_string()
|
||||
};
|
||||
if full_names.contains(&import_name.as_str()) {
|
||||
if let NameOrAttribute::N(name) = &alias.name {
|
||||
let import_name = name.value.to_string();
|
||||
let full_name = body
|
||||
.module
|
||||
.as_ref()
|
||||
.map(compose_module_path)
|
||||
.map(|module_name| format!("{module_name}.{import_name}"))
|
||||
.unwrap_or(import_name);
|
||||
|
||||
if full_names.contains(&full_name.as_str()) {
|
||||
removable.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,17 @@ use crate::checks::CheckCode;
|
||||
use crate::fs;
|
||||
use crate::settings::PythonVersion;
|
||||
|
||||
pub fn load_config(pyproject: &Option<PathBuf>) -> Result<Config> {
|
||||
pub fn load_config(pyproject: &Option<PathBuf>, quiet: bool) -> Result<Config> {
|
||||
match pyproject {
|
||||
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
|
||||
.tool
|
||||
.and_then(|tool| tool.ruff)
|
||||
.unwrap_or_default()),
|
||||
None => {
|
||||
eprintln!("No pyproject.toml found.");
|
||||
eprintln!("Falling back to default configuration...");
|
||||
if !quiet {
|
||||
eprintln!("No pyproject.toml found.");
|
||||
eprintln!("Falling back to default configuration...");
|
||||
}
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::autofix::Fix;
|
||||
|
||||
/// Generate a fix to remove a base from a ClassDef statement.
|
||||
pub fn remove_class_def_base(
|
||||
locator: &mut SourceCodeLocator,
|
||||
locator: &SourceCodeLocator,
|
||||
stmt_at: &Location,
|
||||
expr_at: Location,
|
||||
bases: &[Expr],
|
||||
@@ -101,7 +101,7 @@ pub fn remove_class_def_base(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
|
||||
pub fn remove_super_arguments(locator: &SourceCodeLocator, expr: &Expr) -> Option<Fix> {
|
||||
let range = Range::from_located(expr);
|
||||
let contents = locator.slice_source_code_range(&range);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use once_cell::sync::Lazy;
|
||||
use rustpython_ast::{Expr, ExprKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
@@ -38,7 +37,7 @@ pub fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) {
|
||||
CheckKind::DeprecatedUnittestAlias(attr.to_string(), target.to_string()),
|
||||
Range::from_located(expr),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
check.amend(Fix::replacement(
|
||||
format!("self.{}", target),
|
||||
expr.location,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use rustpython_ast::{Expr, Stmt};
|
||||
|
||||
use crate::ast::helpers;
|
||||
use crate::autofix::fixer;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::pyupgrade;
|
||||
use crate::pyupgrade::checks;
|
||||
@@ -17,9 +16,9 @@ pub fn super_call_with_parameters(checker: &mut Checker, expr: &Expr, func: &Exp
|
||||
.map(|index| checker.parents[*index])
|
||||
.collect();
|
||||
if let Some(mut check) = checks::super_args(scope, &parents, expr, func, args) {
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
if let Some(fix) =
|
||||
pyupgrade::fixes::remove_super_arguments(&mut checker.locator, expr)
|
||||
pyupgrade::fixes::remove_super_arguments(checker.get_locator(), expr)
|
||||
{
|
||||
check.amend(fix);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use rustpython_ast::Expr;
|
||||
|
||||
use crate::ast::types::{CheckLocator, Range};
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::CheckKind;
|
||||
@@ -11,7 +10,7 @@ pub fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, args:
|
||||
if let Some(mut check) =
|
||||
checks::type_of_primitive(func, args, checker.locate_check(Range::from_located(expr)))
|
||||
{
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
if let CheckKind::TypeOfPrimitive(primitive) = &check.kind {
|
||||
check.amend(Fix::replacement(
|
||||
primitive.builtin(),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use rustpython_ast::Expr;
|
||||
|
||||
use crate::ast::types::{CheckLocator, Range};
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::pyupgrade::checks;
|
||||
@@ -10,7 +9,7 @@ pub fn unnecessary_abspath(checker: &mut Checker, expr: &Expr, func: &Expr, args
|
||||
if let Some(mut check) =
|
||||
checks::unnecessary_abspath(func, args, checker.locate_check(Range::from_located(expr)))
|
||||
{
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
check.amend(Fix::replacement(
|
||||
"__file__".to_string(),
|
||||
expr.location,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use rustpython_ast::Expr;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
@@ -14,7 +13,7 @@ pub fn use_pep585_annotation(checker: &mut Checker, expr: &Expr, id: &str) {
|
||||
CheckKind::UsePEP585Annotation(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
check.amend(Fix::replacement(
|
||||
id.to_lowercase(),
|
||||
expr.location,
|
||||
|
||||
@@ -2,7 +2,6 @@ use rustpython_ast::{Constant, Expr, ExprKind, Operator};
|
||||
|
||||
use crate::ast::helpers::match_name_or_attr;
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
@@ -46,7 +45,7 @@ fn union(elts: &[Expr]) -> Expr {
|
||||
pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, slice: &Expr) {
|
||||
if match_name_or_attr(value, "Optional") {
|
||||
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
let mut generator = SourceGenerator::new();
|
||||
if let Ok(()) = generator.unparse_expr(&optional(slice), 0) {
|
||||
if let Ok(content) = generator.generate() {
|
||||
@@ -61,7 +60,7 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s
|
||||
checker.add_check(check);
|
||||
} else if match_name_or_attr(value, "Union") {
|
||||
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
match &slice.node {
|
||||
ExprKind::Slice { .. } => {
|
||||
// Invalid type annotation.
|
||||
|
||||
@@ -2,7 +2,7 @@ use log::error;
|
||||
use rustpython_ast::{Expr, Stmt};
|
||||
|
||||
use crate::ast::types::{CheckLocator, Range};
|
||||
use crate::autofix::{fixer, helpers};
|
||||
use crate::autofix::helpers;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::pyupgrade::checks;
|
||||
|
||||
@@ -12,7 +12,7 @@ pub fn useless_metaclass_type(checker: &mut Checker, stmt: &Stmt, value: &Expr,
|
||||
value,
|
||||
checker.locate_check(Range::from_located(stmt)),
|
||||
) {
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
let context = checker.binding_context();
|
||||
let deleted: Vec<&Stmt> = checker
|
||||
.deletions
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use rustpython_ast::{Expr, Keyword, Stmt};
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::pyupgrade;
|
||||
use crate::pyupgrade::checks;
|
||||
@@ -14,9 +13,9 @@ pub fn useless_object_inheritance(
|
||||
) {
|
||||
let scope = checker.current_scope();
|
||||
if let Some(mut check) = checks::useless_object_inheritance(name, bases, scope) {
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
if let Some(fix) = pyupgrade::fixes::remove_class_def_base(
|
||||
&mut checker.locator,
|
||||
checker.get_locator(),
|
||||
&stmt.location,
|
||||
check.location,
|
||||
bases,
|
||||
|
||||
@@ -130,8 +130,9 @@ impl RawSettings {
|
||||
pub fn from_pyproject(
|
||||
pyproject: &Option<PathBuf>,
|
||||
project_root: &Option<PathBuf>,
|
||||
quiet: bool,
|
||||
) -> Result<Self> {
|
||||
let config = load_config(pyproject)?;
|
||||
let config = load_config(pyproject, quiet)?;
|
||||
Ok(RawSettings {
|
||||
dummy_variable_rgx: match config.dummy_variable_rgx {
|
||||
Some(pattern) => Regex::new(&pattern)
|
||||
@@ -159,7 +160,7 @@ impl RawSettings {
|
||||
.filter(|code| {
|
||||
matches!(
|
||||
code.category(),
|
||||
CheckCategory::Pycodestyle | CheckCategory::Pyflakes
|
||||
CheckCategory::PycodestyleError | CheckCategory::Pyflakes
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
||||
21
src/snapshots/ruff__linter__tests__B002_B002.py.snap
Normal file
21
src/snapshots/ruff__linter__tests__B002_B002.py.snap
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnaryPrefixIncrement
|
||||
location:
|
||||
row: 15
|
||||
column: 9
|
||||
end_location:
|
||||
row: 15
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind: UnaryPrefixIncrement
|
||||
location:
|
||||
row: 20
|
||||
column: 12
|
||||
end_location:
|
||||
row: 20
|
||||
column: 15
|
||||
fix: ~
|
||||
|
||||
77
src/snapshots/ruff__linter__tests__B007_B007.py.snap
Normal file
77
src/snapshots/ruff__linter__tests__B007_B007.py.snap
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
UnusedLoopControlVariable: i
|
||||
location:
|
||||
row: 6
|
||||
column: 5
|
||||
end_location:
|
||||
row: 6
|
||||
column: 6
|
||||
fix:
|
||||
patch:
|
||||
content: _i
|
||||
location:
|
||||
row: 6
|
||||
column: 5
|
||||
end_location:
|
||||
row: 6
|
||||
column: 6
|
||||
applied: false
|
||||
- kind:
|
||||
UnusedLoopControlVariable: k
|
||||
location:
|
||||
row: 18
|
||||
column: 13
|
||||
end_location:
|
||||
row: 18
|
||||
column: 14
|
||||
fix:
|
||||
patch:
|
||||
content: _k
|
||||
location:
|
||||
row: 18
|
||||
column: 13
|
||||
end_location:
|
||||
row: 18
|
||||
column: 14
|
||||
applied: false
|
||||
- kind:
|
||||
UnusedLoopControlVariable: i
|
||||
location:
|
||||
row: 30
|
||||
column: 5
|
||||
end_location:
|
||||
row: 30
|
||||
column: 6
|
||||
fix:
|
||||
patch:
|
||||
content: _i
|
||||
location:
|
||||
row: 30
|
||||
column: 5
|
||||
end_location:
|
||||
row: 30
|
||||
column: 6
|
||||
applied: false
|
||||
- kind:
|
||||
UnusedLoopControlVariable: k
|
||||
location:
|
||||
row: 30
|
||||
column: 13
|
||||
end_location:
|
||||
row: 30
|
||||
column: 14
|
||||
fix:
|
||||
patch:
|
||||
content: _k
|
||||
location:
|
||||
row: 30
|
||||
column: 13
|
||||
end_location:
|
||||
row: 30
|
||||
column: 14
|
||||
applied: false
|
||||
|
||||
13
src/snapshots/ruff__linter__tests__B017_B017.py.snap
Normal file
13
src/snapshots/ruff__linter__tests__B017_B017.py.snap
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: NoAssertRaisesException
|
||||
location:
|
||||
row: 22
|
||||
column: 9
|
||||
end_location:
|
||||
row: 25
|
||||
column: 5
|
||||
fix: ~
|
||||
|
||||
@@ -51,13 +51,13 @@ expression: checks
|
||||
column: 24
|
||||
fix:
|
||||
patch:
|
||||
content: import logging.handlers
|
||||
content: ""
|
||||
location:
|
||||
row: 12
|
||||
column: 1
|
||||
end_location:
|
||||
row: 12
|
||||
column: 24
|
||||
row: 13
|
||||
column: 1
|
||||
applied: false
|
||||
- kind:
|
||||
UnusedImport:
|
||||
|
||||
81
src/snapshots/ruff__linter__tests__F401_F401_5.py.snap
Normal file
81
src/snapshots/ruff__linter__tests__F401_F401_5.py.snap
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- a.b.c
|
||||
location:
|
||||
row: 2
|
||||
column: 1
|
||||
end_location:
|
||||
row: 2
|
||||
column: 18
|
||||
fix:
|
||||
patch:
|
||||
content: ""
|
||||
location:
|
||||
row: 2
|
||||
column: 1
|
||||
end_location:
|
||||
row: 3
|
||||
column: 1
|
||||
applied: false
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- d.e.f
|
||||
location:
|
||||
row: 3
|
||||
column: 1
|
||||
end_location:
|
||||
row: 3
|
||||
column: 23
|
||||
fix:
|
||||
patch:
|
||||
content: ""
|
||||
location:
|
||||
row: 3
|
||||
column: 1
|
||||
end_location:
|
||||
row: 4
|
||||
column: 1
|
||||
applied: false
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- h.i
|
||||
location:
|
||||
row: 4
|
||||
column: 1
|
||||
end_location:
|
||||
row: 4
|
||||
column: 11
|
||||
fix:
|
||||
patch:
|
||||
content: ""
|
||||
location:
|
||||
row: 4
|
||||
column: 1
|
||||
end_location:
|
||||
row: 5
|
||||
column: 1
|
||||
applied: false
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- j.k
|
||||
location:
|
||||
row: 5
|
||||
column: 1
|
||||
end_location:
|
||||
row: 5
|
||||
column: 16
|
||||
fix:
|
||||
patch:
|
||||
content: ""
|
||||
location:
|
||||
row: 5
|
||||
column: 1
|
||||
end_location:
|
||||
row: 6
|
||||
column: 1
|
||||
applied: false
|
||||
|
||||
@@ -4,13 +4,35 @@ expression: checks
|
||||
---
|
||||
- kind:
|
||||
ConstantImportedAsNonConstant:
|
||||
- BAD
|
||||
- bad
|
||||
- CONST
|
||||
- const
|
||||
location:
|
||||
row: 1
|
||||
column: 1
|
||||
end_location:
|
||||
row: 1
|
||||
column: 27
|
||||
column: 26
|
||||
fix: ~
|
||||
- kind:
|
||||
ConstantImportedAsNonConstant:
|
||||
- CONSTANT
|
||||
- constant
|
||||
location:
|
||||
row: 2
|
||||
column: 1
|
||||
end_location:
|
||||
row: 2
|
||||
column: 37
|
||||
fix: ~
|
||||
- kind:
|
||||
ConstantImportedAsNonConstant:
|
||||
- ANOTHER_CONSTANT
|
||||
- another_constant
|
||||
location:
|
||||
row: 3
|
||||
column: 1
|
||||
end_location:
|
||||
row: 3
|
||||
column: 53
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -4,13 +4,35 @@ expression: checks
|
||||
---
|
||||
- kind:
|
||||
LowercaseImportedAsNonLowercase:
|
||||
- bad
|
||||
- Bad
|
||||
- lowercase
|
||||
- Lower
|
||||
location:
|
||||
row: 1
|
||||
column: 1
|
||||
end_location:
|
||||
row: 1
|
||||
column: 27
|
||||
column: 31
|
||||
fix: ~
|
||||
- kind:
|
||||
LowercaseImportedAsNonLowercase:
|
||||
- lowercase
|
||||
- Lowercase
|
||||
location:
|
||||
row: 2
|
||||
column: 1
|
||||
end_location:
|
||||
row: 2
|
||||
column: 39
|
||||
fix: ~
|
||||
- kind:
|
||||
LowercaseImportedAsNonLowercase:
|
||||
- another_lowercase
|
||||
- AnotherLowercase
|
||||
location:
|
||||
row: 3
|
||||
column: 1
|
||||
end_location:
|
||||
row: 3
|
||||
column: 54
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -4,13 +4,35 @@ expression: checks
|
||||
---
|
||||
- kind:
|
||||
CamelcaseImportedAsLowercase:
|
||||
- CamelCase
|
||||
- camelcase
|
||||
- Camel
|
||||
- camel
|
||||
location:
|
||||
row: 1
|
||||
column: 1
|
||||
end_location:
|
||||
row: 1
|
||||
column: 26
|
||||
fix: ~
|
||||
- kind:
|
||||
CamelcaseImportedAsLowercase:
|
||||
- CamelCase
|
||||
- camelcase
|
||||
location:
|
||||
row: 2
|
||||
column: 1
|
||||
end_location:
|
||||
row: 2
|
||||
column: 39
|
||||
fix: ~
|
||||
- kind:
|
||||
CamelcaseImportedAsLowercase:
|
||||
- AnotherCamelCase
|
||||
- another_camelcase
|
||||
location:
|
||||
row: 3
|
||||
column: 1
|
||||
end_location:
|
||||
row: 3
|
||||
column: 54
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -4,13 +4,35 @@ expression: checks
|
||||
---
|
||||
- kind:
|
||||
CamelcaseImportedAsConstant:
|
||||
- CamelCase
|
||||
- CAMELCASE
|
||||
- Camel
|
||||
- CAMEL
|
||||
location:
|
||||
row: 1
|
||||
column: 1
|
||||
end_location:
|
||||
row: 1
|
||||
column: 26
|
||||
fix: ~
|
||||
- kind:
|
||||
CamelcaseImportedAsConstant:
|
||||
- CamelCase
|
||||
- CAMELCASE
|
||||
location:
|
||||
row: 2
|
||||
column: 1
|
||||
end_location:
|
||||
row: 2
|
||||
column: 39
|
||||
fix: ~
|
||||
- kind:
|
||||
CamelcaseImportedAsConstant:
|
||||
- AnotherCamelCase
|
||||
- ANOTHER_CAMELCASE
|
||||
location:
|
||||
row: 3
|
||||
column: 1
|
||||
end_location:
|
||||
row: 3
|
||||
column: 54
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -4,13 +4,24 @@ expression: checks
|
||||
---
|
||||
- kind:
|
||||
CamelcaseImportedAsAcronym:
|
||||
- CamelCase
|
||||
- CC
|
||||
- CaMel
|
||||
- CM
|
||||
location:
|
||||
row: 1
|
||||
column: 1
|
||||
end_location:
|
||||
row: 1
|
||||
column: 23
|
||||
fix: ~
|
||||
- kind:
|
||||
CamelcaseImportedAsAcronym:
|
||||
- CamelCase
|
||||
- CC
|
||||
location:
|
||||
row: 2
|
||||
column: 1
|
||||
end_location:
|
||||
row: 2
|
||||
column: 32
|
||||
fix: ~
|
||||
|
||||
|
||||
41
src/snapshots/ruff__linter__tests__W605_W605.py.snap
Normal file
41
src/snapshots/ruff__linter__tests__W605_W605.py.snap
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
InvalidEscapeSequence: "."
|
||||
location:
|
||||
row: 2
|
||||
column: 10
|
||||
end_location:
|
||||
row: 2
|
||||
column: 11
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidEscapeSequence: "."
|
||||
location:
|
||||
row: 6
|
||||
column: 1
|
||||
end_location:
|
||||
row: 6
|
||||
column: 2
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidEscapeSequence: _
|
||||
location:
|
||||
row: 11
|
||||
column: 6
|
||||
end_location:
|
||||
row: 11
|
||||
column: 7
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidEscapeSequence: _
|
||||
location:
|
||||
row: 18
|
||||
column: 6
|
||||
end_location:
|
||||
row: 18
|
||||
column: 7
|
||||
fix: ~
|
||||
|
||||
Reference in New Issue
Block a user