Compare commits

..

34 Commits

Author SHA1 Message Date
Charlie Marsh
3f20cea402 Bump version to 0.0.86 2022-10-27 13:09:57 -04:00
Charlie Marsh
389fe1ff64 Avoid auto-fixing unused imports in __init__.py (#489) 2022-10-27 13:08:04 -04:00
Charlie Marsh
bad2d7ba85 Add example of per-file ignores to the README (#488) 2022-10-27 12:58:52 -04:00
Charlie Marsh
416aa298ac Allow whitespace in per-file ignore patterns (#487) 2022-10-27 12:55:28 -04:00
Charlie Marsh
a535b1adbf Replace compliance comments with check codes (#485) 2022-10-27 09:32:18 -04:00
Charlie Marsh
05fbd1a283 Bump version to 0.0.85 2022-10-26 19:13:04 -04:00
Charlie Marsh
63552cbc8e Implement W605 (invalid escape sequence) (#482) 2022-10-26 19:10:24 -04:00
Charlie Marsh
c00bd489f1 Fix multi-segment import removal (#480) 2022-10-26 16:43:55 -04:00
Charlie Marsh
16c2e3a995 Handle multi-segment import-from removal (#479) 2022-10-26 16:36:12 -04:00
Anders Kaseorg
650b025181 Suppress “No pyproject.toml found” message with --quiet (#478) 2022-10-26 16:03:25 -04:00
Anders Kaseorg
8fe46f7400 Rename --quiet to --silent and make --quiet only log errors (#477) 2022-10-26 16:03:10 -04:00
Charlie Marsh
a9bcc15797 Bump version to 0.0.84 2022-10-26 12:01:55 -04:00
Charlie Marsh
b6c856bd07 Implement B007 (unused loop control variable) (#473) 2022-10-26 12:01:27 -04:00
Charlie Marsh
4beea0484a Use lazy initialization for SourceCodeLocator (#472) 2022-10-26 11:27:48 -04:00
Suguru Yamamoto
2679db1d10 Correct EOL offset for lines ending with multi-byte char (#471) 2022-10-26 11:00:27 -04:00
Charlie Marsh
3e73462e04 Optimize imports 2022-10-25 22:06:51 -04:00
Charlie Marsh
f63a87737a DRY up usages of matches with fixer Mode (#470) 2022-10-25 22:02:58 -04:00
Charlie Marsh
e7472eac1c Increment flake8-bugbear to 8/32 2022-10-25 21:54:00 -04:00
Charlie Marsh
db3c847771 Bump version to 0.0.83 2022-10-25 21:24:13 -04:00
Charlie Marsh
4adbfc24a5 Increment flake8-bugbear to 6/32 2022-10-25 21:23:13 -04:00
Charlie Marsh
8f734a6562 Implement B002 (unary prefix increment) (#468) 2022-10-25 21:10:51 -04:00
Charlie Marsh
bcf7519eb3 Implement B017 (no assertRaises(Exception)) (#467) 2022-10-25 20:55:00 -04:00
Heyward Fann
66089052ee chore: typo on #283 link (#464) 2022-10-25 08:02:11 -04:00
Jeong YunWon
d50cc8ff65 Restyle flake8_comprehensions::check to reduce indent (#462) 2022-10-24 15:16:56 -04:00
Harutaka Kawamura
b75ea94f58 Fix uppercase and lowercase check (#461) 2022-10-22 12:49:13 -04:00
Harutaka Kawamura
2c24e2fd28 Enable N811, 812, 813, 814, 817 for Import (#460) 2022-10-22 08:31:02 -04:00
Charlie Marsh
f8dc208665 Tweak messages for pep8-naming rules 2022-10-21 17:06:16 -04:00
Charlie Marsh
c72b8e8d1e Bump version to 0.0.82 2022-10-21 12:15:19 -04:00
Harutaka Kawamura
b108c693fa Implement N811, 812, 813, 814, and 817 (#457) 2022-10-20 12:20:44 -04:00
Harutaka Kawamura
aac1912ea7 Implement N807 (#456) 2022-10-20 11:12:02 -04:00
Charlie Marsh
3dcd26aac3 Add SectionUnderlineAfterName to autofix list 2022-10-17 22:08:49 -04:00
Charlie Marsh
e53b9807f6 Bump version to 0.0.81 2022-10-17 21:43:49 -04:00
Charlie Marsh
36fe8b76d4 Enable autofix for over- and under-indented docstrings (#451) 2022-10-17 21:43:38 -04:00
Charlie Marsh
f832f88c75 Implement autofix support for D214, D405, D406, and D416 (#450) 2022-10-17 17:37:20 -04:00
74 changed files with 2286 additions and 713 deletions

2
Cargo.lock generated
View File

@@ -2045,7 +2045,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.80"
version = "0.0.86"
dependencies = [
"anyhow",
"assert_cmd",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.80"
version = "0.0.86"
edition = "2021"
[lib]

View File

@@ -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.80
rev: v0.0.86
hooks:
- id: lint
```
@@ -95,6 +95,10 @@ select = [
"F401",
"F403",
]
per-file-ignores = [
"__init__.py:F401",
"path/to/file.py:F401"
]
```
Alternatively, on the command-line:
@@ -257,7 +261,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 +279,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
@@ -296,24 +306,24 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | 🛠 |
| D205 | BlankLineAfterSummary | 1 blank line required between summary line and description | 🛠 |
| D206 | IndentWithSpaces | Docstring should be indented with spaces, not tabs | |
| D207 | NoUnderIndentation | Docstring is under-indented | |
| D208 | NoOverIndentation | Docstring is over-indented | |
| D207 | NoUnderIndentation | Docstring is under-indented | 🛠 |
| D208 | NoOverIndentation | Docstring is over-indented | 🛠 |
| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | 🛠 |
| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | 🛠 |
| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | 🛠 |
| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | |
| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | |
| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | |
| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | 🛠 |
| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | 🛠 |
| D300 | UsesTripleQuotes | Use """triple double quotes""" | |
| D400 | EndsInPeriod | First line should end with a period | |
| D402 | NoSignature | First line should not be the function's 'signature' | |
| D402 | NoSignature | First line should not be the function's signature | |
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | |
| D404 | NoThisPrefix | First word of the docstring should not be `This` | |
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | |
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | |
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | 🛠 |
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | 🛠 |
| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | 🛠 |
| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | |
| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | 🛠 |
| D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | 🛠 |
| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | 🛠 |
| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | 🛠 |
@@ -321,7 +331,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | 🛠 |
| D414 | NonEmptySection | Section has no content ("Returns") | |
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | |
| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | |
| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | 🛠 |
| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | |
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | |
| D419 | NonEmpty | Docstring is empty | |
@@ -348,6 +358,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 `...` | |
### flake8-comprehensions
@@ -374,8 +390,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
@@ -465,7 +484,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)
@@ -485,7 +504,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).
@@ -506,7 +525,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?

View File

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

15
resources/test/fixtures/N807.py vendored Normal file
View File

@@ -0,0 +1,15 @@
def __bad__():
pass
def __good():
pass
def good__():
pass
class Class:
def __good__(self):
pass

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

@@ -0,0 +1,3 @@
import mod.CONST as const
from mod import CONSTANT as constant
from mod import ANOTHER_CONSTANT as another_constant

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

@@ -0,0 +1,3 @@
import modl.lowercase as Lower
from mod import lowercase as Lowercase
from mod import another_lowercase as AnotherLowercase

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

@@ -0,0 +1,3 @@
import mod.Camel as camel
from mod import CamelCase as camelcase
from mod import AnotherCamelCase as another_camelcase

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

@@ -0,0 +1,3 @@
import mod.Camel as CAMEL
from mod import CamelCase as CAMELCASE
from mod import AnotherCamelCase as ANOTHER_CAMELCASE

2
resources/test/fixtures/N817.py vendored Normal file
View File

@@ -0,0 +1,2 @@
import mod.CaMel as CM
from mod import CamelCase as CC

35
resources/test/fixtures/W605.py vendored Normal file
View 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

View File

@@ -1,3 +1,5 @@
import os
print(__path__)
__all__ = ["a", "b", "c"]

View File

@@ -5,3 +5,6 @@ extend-exclude = [
"migrations",
"directory/also_excluded.py",
]
per-file-ignores = [
"__init__.py:F401",
]

View File

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

View File

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

View File

@@ -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>
@@ -245,6 +262,14 @@ where
}
}
if self.settings.enabled.contains(&CheckCode::N807) {
if let Some(check) =
pep8_naming::checks::dunder_function_name(stmt, self.current_scope(), name)
{
self.checks.push(check);
}
}
self.check_builtin_shadowing(name, Range::from_located(stmt), true);
// Visit the decorators and arguments, but avoid the body, which will be deferred.
@@ -405,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 {
@@ -525,6 +599,64 @@ where
},
)
}
if let Some(asname) = &alias.node.asname {
if self.settings.enabled.contains(&CheckCode::N811) {
if let Some(check) =
pep8_naming::checks::constant_imported_as_non_constant(
stmt,
&alias.node.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,
&alias.node.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,
&alias.node.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,
&alias.node.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,
&alias.node.name,
asname,
) {
self.checks.push(check);
}
}
}
}
}
StmtKind::Raise { exc, .. } => {
@@ -552,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) {
@@ -1011,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,
@@ -1946,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()
@@ -1959,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);
@@ -1970,15 +2115,27 @@ impl<'a> Checker<'a> {
None
};
let mut check = Check::new(
CheckKind::UnusedImport(full_names.into_iter().map(String::from).collect()),
self.locate_check(Range::from_located(child)),
);
if let Some(fix) = fix {
check.amend(fix);
if self.path.ends_with("__init__.py") {
self.checks.push(Check::new(
CheckKind::UnusedImport(
full_names.into_iter().map(String::from).collect(),
true,
),
self.locate_check(Range::from_located(child)),
));
} else {
let mut check = Check::new(
CheckKind::UnusedImport(
full_names.into_iter().map(String::from).collect(),
false,
),
self.locate_check(Range::from_located(child)),
);
if let Some(fix) = fix {
check.amend(fix);
}
self.checks.push(check);
}
self.checks.push(check);
}
}
}

View File

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

View File

@@ -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,
@@ -158,6 +162,12 @@ pub enum CheckCode {
N803,
N804,
N805,
N807,
N811,
N812,
N813,
N814,
N817,
// Meta
M001,
}
@@ -165,7 +175,8 @@ pub enum CheckCode {
#[derive(EnumIter, Debug, PartialEq, Eq)]
pub enum CheckCategory {
Pyflakes,
Pycodestyle,
PycodestyleError,
PycodestyleWarning,
Pydocstyle,
Pyupgrade,
PEP8Naming,
@@ -179,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",
@@ -196,8 +208,9 @@ impl CheckCategory {
#[allow(clippy::upper_case_acronyms)]
pub enum LintSource {
AST,
Lines,
FileSystem,
Lines,
Tokens,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -225,6 +238,7 @@ pub enum CheckKind {
TypeComparison,
// pycodestyle warnings
NoNewLineAtEndOfFile,
InvalidEscapeSequence(char),
// pyflakes
AssertTuple,
BreakOutsideLoop,
@@ -251,7 +265,7 @@ pub enum CheckKind {
UndefinedExport(String),
UndefinedLocal(String),
UndefinedName(String),
UnusedImport(Vec<String>),
UnusedImport(Vec<String>, bool),
UnusedVariable(String),
YieldOutsideFunction,
// flake8-builtins
@@ -259,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,
@@ -342,6 +359,12 @@ pub enum CheckKind {
InvalidArgumentName(String),
InvalidFirstArgumentNameForClassMethod,
InvalidFirstArgumentNameForMethod,
DunderFunctionName,
ConstantImportedAsNonConstant(String, String),
LowercaseImportedAsNonLowercase(String, String),
CamelcaseImportedAsLowercase(String, String),
CamelcaseImportedAsConstant(String, String),
CamelcaseImportedAsAcronym(String, String),
// Meta
UnusedNOQA(Option<Vec<String>>),
}
@@ -351,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,
}
@@ -376,8 +400,9 @@ 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::F401 => CheckKind::UnusedImport(vec!["...".to_string()], false),
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
CheckCode::F404 => CheckKind::LateFutureImport,
@@ -412,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,
@@ -517,6 +545,22 @@ impl CheckCode {
CheckCode::N803 => CheckKind::InvalidArgumentName("...".to_string()),
CheckCode::N804 => CheckKind::InvalidFirstArgumentNameForClassMethod,
CheckCode::N805 => CheckKind::InvalidFirstArgumentNameForMethod,
CheckCode::N807 => CheckKind::DunderFunctionName,
CheckCode::N811 => {
CheckKind::ConstantImportedAsNonConstant("...".to_string(), "...".to_string())
}
CheckCode::N812 => {
CheckKind::LowercaseImportedAsNonLowercase("...".to_string(), "...".to_string())
}
CheckCode::N813 => {
CheckKind::CamelcaseImportedAsLowercase("...".to_string(), "...".to_string())
}
CheckCode::N814 => {
CheckKind::CamelcaseImportedAsConstant("...".to_string(), "...".to_string())
}
CheckCode::N817 => {
CheckKind::CamelcaseImportedAsAcronym("...".to_string(), "...".to_string())
}
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
@@ -524,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,
@@ -570,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,
@@ -648,6 +696,12 @@ impl CheckCode {
CheckCode::N803 => CheckCategory::PEP8Naming,
CheckCode::N804 => CheckCategory::PEP8Naming,
CheckCode::N805 => CheckCategory::PEP8Naming,
CheckCode::N807 => CheckCategory::PEP8Naming,
CheckCode::N811 => CheckCategory::PEP8Naming,
CheckCode::N812 => CheckCategory::PEP8Naming,
CheckCode::N813 => CheckCategory::PEP8Naming,
CheckCode::N814 => CheckCategory::PEP8Naming,
CheckCode::N817 => CheckCategory::PEP8Naming,
CheckCode::M001 => CheckCategory::Meta,
}
}
@@ -697,18 +751,22 @@ impl CheckKind {
CheckKind::UndefinedExport(_) => &CheckCode::F822,
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedImport(_, _) => &CheckCode::F401,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
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,
@@ -790,6 +848,12 @@ impl CheckKind {
CheckKind::InvalidArgumentName(_) => &CheckCode::N803,
CheckKind::InvalidFirstArgumentNameForClassMethod => &CheckCode::N804,
CheckKind::InvalidFirstArgumentNameForMethod => &CheckCode::N805,
CheckKind::DunderFunctionName => &CheckCode::N807,
CheckKind::ConstantImportedAsNonConstant(..) => &CheckCode::N811,
CheckKind::LowercaseImportedAsNonLowercase(..) => &CheckCode::N812,
CheckKind::CamelcaseImportedAsLowercase(..) => &CheckCode::N813,
CheckKind::CamelcaseImportedAsConstant(..) => &CheckCode::N814,
CheckKind::CamelcaseImportedAsAcronym(..) => &CheckCode::N817,
// Meta
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
}
@@ -916,9 +980,13 @@ impl CheckKind {
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UnusedImport(names) => {
CheckKind::UnusedImport(names, in_init_py) => {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("{names} imported but unused")
if *in_init_py {
format!("{names} imported but unused and missing from `__all__`")
} else {
format!("{names} imported but unused")
}
}
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
@@ -928,6 +996,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")
@@ -939,6 +1008,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()
@@ -952,6 +1023,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}`")
}
@@ -1080,7 +1154,7 @@ impl CheckKind {
"Multi-line docstring summary should start at the second line".to_string()
}
CheckKind::NoSignature => {
"First line should not be the function's 'signature'".to_string()
"First line should not be the function's signature".to_string()
}
CheckKind::NoBlankLineBeforeFunction(num_lines) => {
format!("No blank lines allowed before function docstring (found {num_lines})")
@@ -1180,6 +1254,24 @@ impl CheckKind {
CheckKind::InvalidFirstArgumentNameForMethod => {
"First argument of a method should be named `self`".to_string()
}
CheckKind::DunderFunctionName => {
"Function name should not start and end with `__`".to_string()
}
CheckKind::ConstantImportedAsNonConstant(name, asname) => {
format!("Constant `{name}` imported as non-constant `{asname}`")
}
CheckKind::LowercaseImportedAsNonLowercase(name, asname) => {
format!("Lowercase `{name}` imported as non-lowercase `{asname}`")
}
CheckKind::CamelcaseImportedAsLowercase(name, asname) => {
format!("Camelcase `{name}` imported as lowercase `{asname}`")
}
CheckKind::CamelcaseImportedAsConstant(name, asname) => {
format!("Camelcase `{name}` imported as constant `{asname}`")
}
CheckKind::CamelcaseImportedAsAcronym(name, asname) => {
format!("Camelcase `{name}` imported as acronym `{asname}`")
}
// Meta
CheckKind::UnusedNOQA(codes) => match codes {
None => "Unused `noqa` directive".to_string(),
@@ -1200,6 +1292,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!(
@@ -1208,26 +1316,34 @@ impl CheckKind {
| CheckKind::BlankLineAfterSection(_)
| CheckKind::BlankLineAfterSummary
| CheckKind::BlankLineBeforeSection(_)
| CheckKind::CapitalizeSectionName(_)
| CheckKind::DashedUnderlineAfterSection(_)
| CheckKind::DeprecatedUnittestAlias(_, _)
| CheckKind::DoNotAssertFalse
| CheckKind::DuplicateHandlerException(_)
| CheckKind::NewLineAfterLastParagraph
| CheckKind::NewLineAfterSectionName(_)
| CheckKind::NoBlankLineAfterFunction(_)
| CheckKind::NoBlankLineBeforeClass(_)
| CheckKind::NoBlankLineBeforeFunction(_)
| CheckKind::NoBlankLinesBetweenHeaderAndContent(_)
| CheckKind::NoOverIndentation
| CheckKind::NoSurroundingWhitespace
| CheckKind::NoUnderIndentation
| CheckKind::OneBlankLineAfterClass(_)
| CheckKind::OneBlankLineBeforeClass(_)
| CheckKind::PPrintFound
| CheckKind::PrintFound
| CheckKind::SectionNameEndsInColon(_)
| CheckKind::SectionNotOverIndented(_)
| CheckKind::SectionUnderlineAfterName(_)
| CheckKind::SectionUnderlineMatchesSectionLength(_)
| CheckKind::SectionUnderlineNotOverIndented(_)
| CheckKind::SuperCallWithParameters
| CheckKind::TypeOfPrimitive(_)
| CheckKind::UnnecessaryAbspath
| CheckKind::UnusedImport(_)
| CheckKind::UnusedImport(_, false)
| CheckKind::UnusedLoopControlVariable(_)
| CheckKind::UnusedNOQA(_)
| CheckKind::UsePEP585Annotation(_)
| CheckKind::UsePEP604Annotation

View File

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

@@ -0,0 +1 @@
pub mod helpers;

View File

@@ -25,10 +25,18 @@ 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()),
})
}
/// Replace any non-whitespace characters from an indentation string.
pub fn clean(indentation: &str) -> String {
indentation
.chars()
.map(|char| if char.is_whitespace() { char } else { ' ' })
.collect()
}

View File

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

View 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),
));
}
}
}
}

View File

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

View File

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

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

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

View File

@@ -4,403 +4,308 @@ use rustpython_ast::{Comprehension, Constant, Expr, ExprKind, KeywordData, Locat
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// Check `list(generator)` compliance.
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)
}
/// C400 (`list(generator)`)
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.
/// C401 (`set(generator)`)
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.
/// C402 (`dict((x, y) for x, y in iterable)`)
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
}
/// Check `set([...])` compliance.
/// C403 (`set([...])`)
pub fn unnecessary_list_comprehension_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::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
}
/// Check `dict([...])` compliance.
/// C404 (`dict([...])`)
pub fn unnecessary_list_comprehension_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::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
}
/// Check `set([1, 2])` compliance.
/// C405 (`set([1, 2])`)
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.
/// C406 (`dict([(1, 2)])`)
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),
))
}
/// C408
pub fn unnecessary_collection_call(
expr: &Expr,
func: &Expr,
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 {
"dict" if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) => (),
"list" | "tuple" => {
// list() or tuple()
}
_ => return None,
};
Some(Check::new(
CheckKind::UnnecessaryCollectionCall(id.to_string()),
Range::from_located(expr),
))
}
/// C409
pub fn unnecessary_literal_within_tuple_call(
expr: &Expr,
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),
))
}
/// C410
pub fn unnecessary_literal_within_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() {
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),
))
}
/// C411
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
}
/// C413
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
}
/// C414
pub fn unnecessary_double_cast_or_process(
expr: &Expr,
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
}
/// C415
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),
));
}
}
}
@@ -411,95 +316,72 @@ pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -
None
}
/// C416
pub fn unnecessary_comprehension(
expr: &Expr,
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;
}
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),
))
}
/// C417
pub fn unnecessary_map(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
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));
}
}
}
}
None
}
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),
));
}
}
} 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
}

View File

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

View File

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

View File

@@ -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")]
@@ -358,6 +372,12 @@ mod tests {
#[test_case(CheckCode::N803, Path::new("N803.py"); "N803")]
#[test_case(CheckCode::N804, Path::new("N804.py"); "N804")]
#[test_case(CheckCode::N805, Path::new("N805.py"); "N805")]
#[test_case(CheckCode::N807, Path::new("N807.py"); "N807")]
#[test_case(CheckCode::N811, Path::new("N811.py"); "N811")]
#[test_case(CheckCode::N812, Path::new("N812.py"); "N812")]
#[test_case(CheckCode::N813, Path::new("N813.py"); "N813")]
#[test_case(CheckCode::N814, Path::new("N814.py"); "N814")]
#[test_case(CheckCode::N817, Path::new("N817.py"); "N817")]
#[test_case(CheckCode::T201, Path::new("T201.py"); "T201")]
#[test_case(CheckCode::T203, Path::new("T203.py"); "T203")]
#[test_case(CheckCode::U001, Path::new("U001.py"); "U001")]
@@ -371,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(

View File

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

View File

@@ -1,3 +1,4 @@
use itertools::Itertools;
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::types::{Range, Scope, ScopeKind};
@@ -97,3 +98,164 @@ pub fn invalid_first_argument_name_for_method(
}
None
}
pub fn dunder_function_name(func_def: &Stmt, scope: &Scope, name: &str) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class) {
return None;
}
if name.starts_with("__") && name.ends_with("__") {
return Some(Check::new(
CheckKind::DunderFunctionName,
Range::from_located(func_def),
));
}
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 is_upper(name) && !is_upper(asname) {
return Some(Check::new(
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
));
}
None
}
pub fn lowercase_imported_as_non_lowercase(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
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),
));
}
None
}
fn is_camelcase(name: &str) -> bool {
!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
}
pub fn camelcase_imported_as_lowercase(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_lower(asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
));
}
None
}
pub fn camelcase_imported_as_constant(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
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),
));
}
None
}
pub fn camelcase_imported_as_acronym(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
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),
));
}
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"));
}
}

View File

@@ -8,7 +8,7 @@ fn is_ambiguous_name(name: &str) -> bool {
name == "l" || name == "I" || name == "O"
}
/// Check AmbiguousVariableName compliance.
/// E741
pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
@@ -20,7 +20,7 @@ pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
}
}
/// Check AmbiguousClassName compliance.
/// E742
pub fn ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
@@ -32,7 +32,7 @@ pub fn ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
}
}
/// Check AmbiguousFunctionName compliance.
/// E743
pub fn ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
@@ -44,7 +44,7 @@ pub fn ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
}
}
/// Check DoNotAssignLambda compliance.
/// E731
pub fn do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Lambda { .. } = &value.node {
Some(Check::new(CheckKind::DoNotAssignLambda, location))
@@ -53,7 +53,7 @@ pub fn do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
}
}
/// Check NotInTest and NotIsTest compliance.
/// E713, E714
pub fn not_tests(
op: &Unaryop,
operand: &Expr,
@@ -92,7 +92,7 @@ pub fn not_tests(
checks
}
/// Check TrueFalseComparison and NoneComparison compliance.
/// E711, E712
pub fn literal_comparisons(
left: &Expr,
ops: &[Cmpop],
@@ -201,7 +201,7 @@ pub fn literal_comparisons(
checks
}
/// Check TypeComparison compliance.
/// E721
pub fn type_comparison(ops: &[Cmpop], comparators: &[Expr], location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];

View File

@@ -4,10 +4,8 @@ use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Arg, Constant, ExprKind, Location, StmtKind};
use titlecase::titlecase;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
@@ -163,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()
@@ -180,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),
@@ -192,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)
@@ -218,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(
@@ -247,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()
@@ -267,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),
@@ -284,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(),
@@ -299,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)
@@ -317,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(),
@@ -359,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(),
@@ -386,23 +392,10 @@ pub fn indent(checker: &mut Checker, definition: &Definition) {
return;
}
let mut has_seen_tab = false;
let mut has_seen_over_indent = false;
let mut has_seen_under_indent = false;
let docstring_indent = helpers::indentation(checker, docstring).to_string();
if !has_seen_tab {
if docstring_indent.contains('\t') {
if checker.settings.enabled.contains(&CheckCode::D206) {
checker.add_check(Check::new(
CheckKind::IndentWithSpaces,
Range::from_located(docstring),
));
}
has_seen_tab = true;
}
}
let mut has_seen_tab = docstring_indent.contains('\t');
let mut is_over_indented = true;
let mut over_indented_lines = vec![];
for i in 0..lines.len() {
// First lines and continuations doesn't need any indentation.
if i == 0 || lines[i - 1].ends_with('\\') {
@@ -416,39 +409,105 @@ pub fn indent(checker: &mut Checker, definition: &Definition) {
}
let line_indent = helpers::leading_space(lines[i]);
if !has_seen_tab {
if line_indent.contains('\t') {
if checker.settings.enabled.contains(&CheckCode::D206) {
checker.add_check(Check::new(
CheckKind::IndentWithSpaces,
Range::from_located(docstring),
));
}
has_seen_tab = true;
}
}
if !has_seen_over_indent {
if line_indent.len() > docstring_indent.len() {
if checker.settings.enabled.contains(&CheckCode::D208) {
checker.add_check(Check::new(
CheckKind::NoOverIndentation,
Range::from_located(docstring),
));
}
has_seen_over_indent = true;
}
}
// We only report tab indentation once, so only check if we haven't seen a tab yet.
has_seen_tab = has_seen_tab || line_indent.contains('\t');
if !has_seen_under_indent {
if checker.settings.enabled.contains(&CheckCode::D207) {
// We report under-indentation on every line. This isn't great, but enables
// autofix.
if line_indent.len() < docstring_indent.len() {
if checker.settings.enabled.contains(&CheckCode::D207) {
checker.add_check(Check::new(
CheckKind::NoUnderIndentation,
Range::from_located(docstring),
let mut check = Check::new(
CheckKind::NoUnderIndentation,
Range {
location: Location::new(docstring.location.row() + i, 1),
end_location: Location::new(docstring.location.row() + i, 1),
},
);
if checker.patch() {
check.amend(Fix::replacement(
helpers::clean(&docstring_indent),
Location::new(docstring.location.row() + i, 1),
Location::new(docstring.location.row() + i, 1 + line_indent.len()),
));
}
has_seen_under_indent = true;
checker.add_check(check);
}
}
// Like pydocstyle, we only report over-indentation if either: (1) every line
// (except, optionally, the last line) is over-indented, or (2) the last line (which
// contains the closing quotation marks) is over-indented. We can't know if we've
// achieved that condition until we've viewed all the lines, so for now, just track
// the over-indentation status of every line.
if i < lines.len() - 1 {
if line_indent.len() > docstring_indent.len() {
over_indented_lines.push(i);
} else {
is_over_indented = false;
}
}
}
if checker.settings.enabled.contains(&CheckCode::D206) {
if has_seen_tab {
checker.add_check(Check::new(
CheckKind::IndentWithSpaces,
Range::from_located(docstring),
));
}
}
if checker.settings.enabled.contains(&CheckCode::D208) {
// If every line (except the last) is over-indented...
if is_over_indented {
for i in over_indented_lines {
let line_indent = helpers::leading_space(lines[i]);
if line_indent.len() > docstring_indent.len() {
// We report over-indentation on every line. This isn't great, but
// enables autofix.
let mut check = Check::new(
CheckKind::NoOverIndentation,
Range {
location: Location::new(docstring.location.row() + i, 1),
end_location: Location::new(docstring.location.row() + i, 1),
},
);
if checker.patch() {
check.amend(Fix::replacement(
helpers::clean(&docstring_indent),
Location::new(docstring.location.row() + i, 1),
Location::new(
docstring.location.row() + i,
1 + line_indent.len(),
),
));
}
checker.add_check(check);
}
}
}
// If the last line is over-indented...
if !lines.is_empty() {
let i = lines.len() - 1;
let line_indent = helpers::leading_space(lines[i]);
if line_indent.len() > docstring_indent.len() {
let mut check = Check::new(
CheckKind::NoOverIndentation,
Range {
location: Location::new(docstring.location.row() + i, 1),
end_location: Location::new(docstring.location.row() + i, 1),
},
);
if checker.patch() {
check.amend(Fix::replacement(
helpers::clean(&docstring_indent),
Location::new(docstring.location.row() + i, 1),
Location::new(docstring.location.row() + i, 1 + line_indent.len()),
));
}
checker.add_check(check);
}
}
}
@@ -471,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 != "'''" {
@@ -479,11 +538,12 @@ 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 mut content = "\n".to_string();
content.push_str(helpers::indentation(checker, docstring));
let content = format!(
"\n{}",
helpers::clean(helpers::indentation(checker, docstring))
);
check.amend(Fix::insertion(
content,
Location::new(
@@ -521,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()
@@ -569,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()
@@ -605,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()
@@ -856,12 +916,13 @@ 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 mut content = "".to_string();
content.push_str(helpers::indentation(checker, docstring));
content.push_str(&"-".repeat(context.section_name.len()));
content.push('\n');
let content = format!(
"{}{}\n",
helpers::clean(helpers::indentation(checker, docstring)),
"-".repeat(context.section_name.len())
);
check.amend(Fix::insertion(
content,
Location::new(docstring.location.row() + context.original_index + 1, 1),
@@ -889,12 +950,13 @@ 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 mut content = "".to_string();
content.push_str(helpers::indentation(checker, docstring));
content.push_str(&"-".repeat(context.section_name.len()));
content.push('\n');
let content = format!(
"{}{}\n",
helpers::clean(helpers::indentation(checker, docstring)),
"-".repeat(context.section_name.len())
);
check.amend(Fix::insertion(
content,
Location::new(docstring.location.row() + context.original_index + 1, 1),
@@ -910,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),
@@ -933,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),
@@ -964,12 +1026,13 @@ 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 mut content = "".to_string();
content.push_str(helpers::indentation(checker, docstring));
content.push_str(&"-".repeat(context.section_name.len()));
content.push('\n');
let content = format!(
"{}{}\n",
helpers::clean(helpers::indentation(checker, docstring)),
"-".repeat(context.section_name.len())
);
check.amend(Fix::replacement(
content,
Location::new(
@@ -1001,10 +1064,10 @@ 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(
indentation,
helpers::clean(&indentation),
Location::new(
docstring.location.row()
+ context.original_index
@@ -1050,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(
@@ -1099,25 +1162,61 @@ fn common_section(
if !style
.section_names()
.contains(&context.section_name.as_str())
&& style
.section_names()
.contains(titlecase(&context.section_name).as_str())
{
checker.add_check(Check::new(
CheckKind::CapitalizeSectionName(context.section_name.to_string()),
Range::from_located(docstring),
))
let capitalized_section_name = titlecase::titlecase(&context.section_name);
if style
.section_names()
.contains(capitalized_section_name.as_str())
{
let mut check = Check::new(
CheckKind::CapitalizeSectionName(context.section_name.to_string()),
Range::from_located(docstring),
);
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) {
// Map from bytes to characters.
let section_name_start = &context.line[..index].chars().count();
let section_name_length = &context.section_name.chars().count();
check.amend(Fix::replacement(
capitalized_section_name,
Location::new(
docstring.location.row() + context.original_index,
1 + section_name_start,
),
Location::new(
docstring.location.row() + context.original_index,
1 + section_name_start + section_name_length,
),
))
}
}
checker.add_check(check);
}
}
}
if checker.settings.enabled.contains(&CheckCode::D214) {
if helpers::leading_space(context.line).len()
> helpers::indentation(checker, docstring).len()
{
checker.add_check(Check::new(
let leading_space = helpers::leading_space(context.line);
let indentation = helpers::indentation(checker, docstring).to_string();
if leading_space.len() > indentation.len() {
let mut check = Check::new(
CheckKind::SectionNotOverIndented(context.section_name.to_string()),
Range::from_located(docstring),
))
);
if checker.patch() {
// Replace the existing indentation with whitespace of the appropriate length.
check.amend(Fix::replacement(
helpers::clean(&indentation),
Location::new(docstring.location.row() + context.original_index, 1),
Location::new(
docstring.location.row() + context.original_index,
1 + leading_space.len(),
),
));
};
checker.add_check(check);
}
}
@@ -1133,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(),
@@ -1144,7 +1243,7 @@ fn common_section(
+ context.following_lines.len(),
1,
),
))
));
}
checker.add_check(check);
}
@@ -1154,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(),
@@ -1178,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(),
@@ -1334,10 +1433,31 @@ fn numpy_section(checker: &mut Checker, definition: &Definition, context: &Secti
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
checker.add_check(Check::new(
let mut check = Check::new(
CheckKind::NewLineAfterSectionName(context.section_name.to_string()),
Range::from_located(docstring),
))
);
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.
let suffix_start = &context.line[..index + context.section_name.len()]
.chars()
.count();
let suffix_length = suffix.chars().count();
check.amend(Fix::deletion(
Location::new(
docstring.location.row() + context.original_index,
1 + suffix_start,
),
Location::new(
docstring.location.row() + context.original_index,
1 + suffix_start + suffix_length,
),
));
}
}
checker.add_check(check)
}
}
@@ -1362,10 +1482,32 @@ fn google_section(checker: &mut Checker, definition: &Definition, context: &Sect
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
checker.add_check(Check::new(
let mut check = Check::new(
CheckKind::SectionNameEndsInColon(context.section_name.to_string()),
Range::from_located(docstring),
))
);
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.
let suffix_start = &context.line[..index + context.section_name.len()]
.chars()
.count();
let suffix_length = suffix.chars().count();
check.amend(Fix::replacement(
":".to_string(),
Location::new(
docstring.location.row() + context.original_index,
1 + suffix_start,
),
Location::new(
docstring.location.row() + context.original_index,
1 + suffix_start + suffix_length,
),
));
}
}
checker.add_check(check);
}
}

View File

@@ -10,7 +10,7 @@ use rustpython_parser::ast::{
use crate::ast::types::{BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
/// Check IfTuple compliance.
/// F634
pub fn if_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
@@ -20,7 +20,7 @@ pub fn if_tuple(test: &Expr, location: Range) -> Option<Check> {
None
}
/// Check AssertTuple compliance.
/// F631
pub fn assert_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
@@ -30,7 +30,7 @@ pub fn assert_tuple(test: &Expr, location: Range) -> Option<Check> {
None
}
/// Check UnusedVariable compliance.
/// F841
pub fn unused_variables(
scope: &Scope,
locator: &dyn CheckLocator,
@@ -63,7 +63,7 @@ pub fn unused_variables(
checks
}
/// Check DefaultExceptNotLast compliance.
/// F707
pub fn default_except_not_last(handlers: &[Excepthandler]) -> Option<Check> {
for (idx, handler) in handlers.iter().enumerate() {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
@@ -78,7 +78,7 @@ pub fn default_except_not_last(handlers: &[Excepthandler]) -> Option<Check> {
None
}
/// Check RaiseNotImplemented compliance.
/// F901
pub fn raise_not_implemented(expr: &Expr) -> Option<Check> {
match &expr.node {
ExprKind::Call { func, .. } => {
@@ -105,7 +105,7 @@ pub fn raise_not_implemented(expr: &Expr) -> Option<Check> {
None
}
/// Check DuplicateArgumentName compliance.
/// F831
pub fn duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -153,7 +153,7 @@ fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
}
}
/// Check MultiValueRepeatedKeyLiteral and MultiValueRepeatedKeyVariable compliance.
/// F601, F602
pub fn repeated_keys(
keys: &[Expr],
check_repeated_literals: bool,
@@ -215,7 +215,7 @@ fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// Check IsLiteral compliance.
/// F632
pub fn is_literal(left: &Expr, ops: &[Cmpop], comparators: &[Expr], location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -232,7 +232,7 @@ pub fn is_literal(left: &Expr, ops: &[Cmpop], comparators: &[Expr], location: Ra
checks
}
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
/// F621, F622
pub fn starred_expressions(
elts: &[Expr],
check_too_many_expressions: bool,
@@ -262,7 +262,7 @@ pub fn starred_expressions(
None
}
/// Check BreakOutsideLoop compliance.
/// F701
pub fn break_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
@@ -303,7 +303,7 @@ pub fn break_outside_loop(
}
}
/// Check ContinueOutsideLoop compliance.
/// F702
pub fn continue_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],

View File

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

View File

@@ -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())
}
}
@@ -79,7 +81,7 @@ impl FromStr for StrCheckCodePair {
if tokens.len() != 2 {
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
}
(tokens[0], tokens[1])
(tokens[0].trim(), tokens[1].trim())
};
let code = CheckCode::from_str(code_string)?;
let pattern = pattern_str.into();
@@ -360,7 +362,10 @@ other-attribute = 1
extend_select: vec![],
ignore: vec![],
extend_ignore: vec![],
per_file_ignores: vec![],
per_file_ignores: vec![StrCheckCodePair {
pattern: "__init__.py".to_string(),
code: CheckCode::F401
}],
dummy_variable_rgx: None,
target_version: None,
}
@@ -373,6 +378,8 @@ other-attribute = 1
fn str_check_code_pair_strings() {
let result = StrCheckCodePair::from_str("foo:E501");
assert!(result.is_ok());
let result = StrCheckCodePair::from_str("foo: E501");
assert!(result.is_ok());
let result = StrCheckCodePair::from_str("E501:foo");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("E501");

View File

@@ -5,7 +5,7 @@ use crate::ast::types::{Binding, BindingKind, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pyupgrade::types::Primitive;
/// Check that `super()` has no args
/// U008
pub fn super_args(
scope: &Scope,
parents: &[&Stmt],
@@ -70,7 +70,7 @@ pub fn super_args(
None
}
/// Check UselessMetaclassType compliance.
/// U001
pub fn useless_metaclass_type(targets: &[Expr], value: &Expr, location: Range) -> Option<Check> {
if targets.len() == 1 {
if let ExprKind::Name { id, .. } = targets.first().map(|expr| &expr.node).unwrap() {
@@ -86,7 +86,7 @@ pub fn useless_metaclass_type(targets: &[Expr], value: &Expr, location: Range) -
None
}
/// Check UnnecessaryAbspath compliance.
/// U002
pub fn unnecessary_abspath(func: &Expr, args: &[Expr], location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
@@ -106,7 +106,7 @@ pub fn unnecessary_abspath(func: &Expr, args: &[Expr], location: Range) -> Optio
None
}
/// Check UselessObjectInheritance compliance.
/// U004
pub fn useless_object_inheritance(name: &str, bases: &[Expr], scope: &Scope) -> Option<Check> {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
@@ -131,7 +131,7 @@ pub fn useless_object_inheritance(name: &str, bases: &[Expr], scope: &Scope) ->
None
}
/// Check TypeOfPrimitive compliance.
/// U003
pub fn type_of_primitive(func: &Expr, args: &[Expr], location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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: ~

View 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

View 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: ~

View File

@@ -4,26 +4,70 @@ expression: checks
---
- kind: NoUnderIndentation
location:
row: 225
column: 5
row: 227
column: 1
end_location:
row: 229
column: 8
fix: ~
row: 227
column: 1
fix:
patch:
content: " "
location:
row: 227
column: 1
end_location:
row: 227
column: 1
applied: false
- kind: NoUnderIndentation
location:
row: 235
column: 5
row: 238
column: 1
end_location:
row: 239
column: 4
fix: ~
row: 238
column: 1
fix:
patch:
content: " "
location:
row: 238
column: 1
end_location:
row: 238
column: 1
applied: false
- kind: NoUnderIndentation
location:
row: 433
column: 37
row: 435
column: 1
end_location:
row: 435
column: 1
fix:
patch:
content: " "
location:
row: 435
column: 1
end_location:
row: 435
column: 5
applied: false
- kind: NoUnderIndentation
location:
row: 436
column: 1
end_location:
row: 436
column: 8
fix: ~
column: 1
fix:
patch:
content: " "
location:
row: 436
column: 1
end_location:
row: 436
column: 5
applied: false

View File

@@ -4,26 +4,53 @@ expression: checks
---
- kind: NoOverIndentation
location:
row: 245
column: 5
row: 247
column: 1
end_location:
row: 249
column: 8
fix: ~
row: 247
column: 1
fix:
patch:
content: " "
location:
row: 247
column: 1
end_location:
row: 247
column: 8
applied: false
- kind: NoOverIndentation
location:
row: 255
column: 5
row: 259
column: 1
end_location:
row: 259
column: 12
fix: ~
column: 1
fix:
patch:
content: " "
location:
row: 259
column: 1
end_location:
row: 259
column: 9
applied: false
- kind: NoOverIndentation
location:
row: 265
column: 5
row: 267
column: 1
end_location:
row: 269
column: 8
fix: ~
row: 267
column: 1
fix:
patch:
content: " "
location:
row: 267
column: 1
end_location:
row: 267
column: 9
applied: false

View File

@@ -10,5 +10,14 @@ expression: checks
end_location:
row: 141
column: 8
fix: ~
fix:
patch:
content: " "
location:
row: 137
column: 1
end_location:
row: 137
column: 9
applied: false

View File

@@ -10,7 +10,16 @@ expression: checks
end_location:
row: 23
column: 8
fix: ~
fix:
patch:
content: Returns
location:
row: 19
column: 5
end_location:
row: 19
column: 12
applied: false
- kind:
CapitalizeSectionName: Short summary
location:
@@ -19,5 +28,14 @@ expression: checks
end_location:
row: 221
column: 8
fix: ~
fix:
patch:
content: Short Summary
location:
row: 209
column: 5
end_location:
row: 209
column: 18
applied: false

View File

@@ -10,7 +10,16 @@ expression: checks
end_location:
row: 36
column: 8
fix: ~
fix:
patch:
content: ""
location:
row: 32
column: 12
end_location:
row: 32
column: 13
applied: false
- kind:
NewLineAfterSectionName: Raises
location:
@@ -19,7 +28,16 @@ expression: checks
end_location:
row: 221
column: 8
fix: ~
fix:
patch:
content: ""
location:
row: 218
column: 11
end_location:
row: 218
column: 12
applied: false
- kind:
NewLineAfterSectionName: Returns
location:
@@ -28,7 +46,16 @@ expression: checks
end_location:
row: 262
column: 8
fix: ~
fix:
patch:
content: ""
location:
row: 257
column: 12
end_location:
row: 257
column: 13
applied: false
- kind:
NewLineAfterSectionName: Raises
location:
@@ -37,5 +64,14 @@ expression: checks
end_location:
row: 262
column: 8
fix: ~
fix:
patch:
content: ""
location:
row: 259
column: 11
end_location:
row: 259
column: 12
applied: false

View File

@@ -4,7 +4,8 @@ expression: checks
---
- kind:
UnusedImport:
- functools
- - functools
- false
location:
row: 2
column: 1
@@ -23,7 +24,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- collections.OrderedDict
- - collections.OrderedDict
- false
location:
row: 4
column: 1
@@ -42,7 +44,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- logging.handlers
- - logging.handlers
- false
location:
row: 12
column: 1
@@ -51,17 +54,18 @@ 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:
- shelve
- - shelve
- false
location:
row: 33
column: 5
@@ -80,7 +84,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- importlib
- - importlib
- false
location:
row: 34
column: 5
@@ -99,7 +104,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- pathlib
- - pathlib
- false
location:
row: 38
column: 5
@@ -118,7 +124,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- pickle
- - pickle
- false
location:
row: 53
column: 9

View File

@@ -0,0 +1,85 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnusedImport:
- - a.b.c
- false
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
- false
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
- false
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
- false
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

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: DunderFunctionName
location:
row: 1
column: 1
end_location:
row: 5
column: 1
fix: ~

View File

@@ -0,0 +1,38 @@
---
source: src/linter.rs
expression: checks
---
- kind:
ConstantImportedAsNonConstant:
- CONST
- const
location:
row: 1
column: 1
end_location:
row: 1
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: ~

View File

@@ -0,0 +1,38 @@
---
source: src/linter.rs
expression: checks
---
- kind:
LowercaseImportedAsNonLowercase:
- lowercase
- Lower
location:
row: 1
column: 1
end_location:
row: 1
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: ~

View File

@@ -0,0 +1,38 @@
---
source: src/linter.rs
expression: checks
---
- kind:
CamelcaseImportedAsLowercase:
- 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: ~

View File

@@ -0,0 +1,38 @@
---
source: src/linter.rs
expression: checks
---
- kind:
CamelcaseImportedAsConstant:
- 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: ~

View File

@@ -0,0 +1,27 @@
---
source: src/linter.rs
expression: checks
---
- kind:
CamelcaseImportedAsAcronym:
- 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: ~

View 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: ~

View File

@@ -4,7 +4,8 @@ expression: checks
---
- kind:
UnusedImport:
- models.Nut
- - models.Nut
- false
location:
row: 5
column: 1